Spaces:
Sleeping
Sleeping
#!/usr/bin/env python3 | |
""" | |
Ethereum MCP Server with Gradio Interface | |
""" | |
import json | |
import logging | |
import os | |
from typing import Any, Dict, List, Optional | |
from dataclasses import dataclass | |
import gradio as gr | |
from web3 import Web3 | |
from eth_tester import EthereumTester | |
from web3.providers.eth_tester import EthereumTesterProvider | |
from eth_account import Account | |
from solcx import compile_source, install_solc | |
import solcx | |
from eth_utils import to_checksum_address | |
# Configure logging | |
logging.basicConfig(level=logging.INFO) | |
logger = logging.getLogger(__name__) | |
def validate_ethereum_address(address: str) -> bool: | |
"""Validate Ethereum address format""" | |
try: | |
if not address or not isinstance(address, str): | |
return False | |
Web3.to_checksum_address(address) | |
return True | |
except (ValueError): | |
return False | |
class EthereumConfig: | |
"""Configuration for Ethereum connections""" | |
use_testnet: bool = True | |
default_network: str = "local_testnet" | |
infura_project_id: Optional[str] = None | |
alchemy_api_key: Optional[str] = None | |
class EthereumClient: | |
"""Ethereum blockchain client with support for multiple networks""" | |
def __init__(self, config: EthereumConfig): | |
self.config = config | |
self.current_network = "local_testnet" | |
self.accounts = {} | |
self.w3 = None | |
self.eth_tester = None | |
self.is_mainnet_readonly = False | |
# Initialize with local testnet by default | |
self._connect_local_testnet() | |
# Install Solidity compiler for contract compilation | |
try: | |
install_solc('0.8.19') | |
solcx.set_solc_version('0.8.19') | |
except Exception as e: | |
logger.warning(f"Could not install Solidity compiler: {e}") | |
def _connect_local_testnet(self): | |
"""Connect to local test blockchain""" | |
try: | |
self.eth_tester = EthereumTester() | |
self.w3 = Web3(EthereumTesterProvider(self.eth_tester)) | |
self.current_network = "local_testnet" | |
self.is_mainnet_readonly = False | |
self.accounts = {} | |
# Fund some test accounts | |
self._setup_test_accounts() | |
logger.info(f"β Connected to Local TestNet: {self.w3.is_connected()}") | |
logger.info(f"π Latest block: {self.w3.eth.block_number}") | |
return True | |
except Exception as e: | |
logger.error(f"Error connecting to local testnet: {e}") | |
return False | |
def _connect_ethereum_mainnet(self, provider_url: str): | |
"""Connect to Ethereum mainnet (read-only for safety)""" | |
try: | |
self.w3 = Web3(Web3.HTTPProvider(provider_url)) | |
if self.w3.is_connected(): | |
self.current_network = "ethereum_mainnet" | |
self.is_mainnet_readonly = True | |
self.accounts = {} # Clear test accounts for safety | |
self.eth_tester = None | |
logger.info(f"β Connected to Ethereum Mainnet: {self.w3.is_connected()}") | |
logger.info(f"π Latest block: {self.w3.eth.block_number}") | |
logger.info("β οΈ MAINNET MODE: Read-only operations only for safety") | |
return True | |
return False | |
except Exception as e: | |
logger.error(f"Error connecting to mainnet: {e}") | |
return False | |
def _connect_ethereum_sepolia(self, provider_url: str): | |
"""Connect to Ethereum Sepolia testnet""" | |
try: | |
self.w3 = Web3(Web3.HTTPProvider(provider_url)) | |
if self.w3.is_connected(): | |
self.current_network = "ethereum_sepolia" | |
self.is_mainnet_readonly = False | |
self.accounts = {} # Clear local test accounts | |
self.eth_tester = None | |
logger.info(f"β Connected to Ethereum Sepolia: {self.w3.is_connected()}") | |
logger.info(f"π Latest block: {self.w3.eth.block_number}") | |
return True | |
return False | |
except Exception as e: | |
logger.error(f"Error connecting to Sepolia: {e}") | |
return False | |
def switch_network(self, network: str, api_key: str = None) -> dict: | |
"""Switch between different Ethereum networks""" | |
try: | |
if network == "local_testnet": | |
success = self._connect_local_testnet() | |
return { | |
"success": success, | |
"network": self.current_network, | |
"readonly": self.is_mainnet_readonly, | |
"message": "Connected to local testnet" if success else "Failed to connect" | |
} | |
elif network == "ethereum_mainnet": | |
if not api_key: | |
return {"success": False, "error": "API key required for mainnet"} | |
# Try Infura first, then Alchemy | |
provider_url = f"https://mainnet.infura.io/v3/{api_key}" | |
success = self._connect_ethereum_mainnet(provider_url) | |
if not success: | |
provider_url = f"https://eth-mainnet.g.alchemy.com/v2/{api_key}" | |
success = self._connect_ethereum_mainnet(provider_url) | |
return { | |
"success": success, | |
"network": self.current_network, | |
"readonly": self.is_mainnet_readonly, | |
"message": "Connected to Ethereum mainnet (read-only)" if success else "Failed to connect to mainnet" | |
} | |
elif network == "ethereum_sepolia": | |
if not api_key: | |
return {"success": False, "error": "API key required for Sepolia"} | |
# Try Infura first, then Alchemy | |
provider_url = f"https://sepolia.infura.io/v3/{api_key}" | |
success = self._connect_ethereum_sepolia(provider_url) | |
if not success: | |
provider_url = f"https://eth-sepolia.g.alchemy.com/v2/{api_key}" | |
success = self._connect_ethereum_sepolia(provider_url) | |
return { | |
"success": success, | |
"network": self.current_network, | |
"readonly": self.is_mainnet_readonly, | |
"message": "Connected to Ethereum Sepolia testnet" if success else "Failed to connect to Sepolia" | |
} | |
else: | |
return {"success": False, "error": f"Unknown network: {network}"} | |
except Exception as e: | |
return {"success": False, "error": str(e)} | |
def _setup_test_accounts(self): | |
"""Set up test accounts with initial funding (local testnet only)""" | |
if self.current_network != "local_testnet" or not self.eth_tester: | |
return | |
test_accounts = self.eth_tester.get_accounts() | |
# Create and fund some test accounts | |
for i in range(3): | |
account_name = f"test_account_{i+1}" | |
new_account = Account.create() | |
try: | |
tx_hash = self.w3.eth.send_transaction({ | |
'from': test_accounts[0], | |
'to': new_account.address, | |
'value': self.w3.to_wei(100, 'ether'), | |
'gas': 21000, | |
'gasPrice': self.w3.to_wei(20, 'gwei') | |
}) | |
self.accounts[account_name] = { | |
"address": new_account.address, | |
"private_key": new_account.key.hex() | |
} | |
logger.info(f"Created and funded: {account_name} - {new_account.address}") | |
except Exception as e: | |
logger.error(f"Error funding test account: {e}") | |
def compile_and_deploy_contract(self, source_code: str, contract_name: str, from_account: str, constructor_args: list = None) -> dict: | |
""" | |
Compile and deploy a smart contract. | |
Only works on local testnet for safety. | |
Args: | |
source_code (str): Solidity source code | |
contract_name (str): Name of the contract to deploy | |
from_account (str): Account name or Eth address to deploy from | |
constructor_args (list): Constructor arguments if any | |
Returns: | |
dict: Deployment result | |
""" | |
try: | |
if self.is_mainnet_readonly: | |
return { | |
"success": False, | |
"error": "Contract deployment disabled on mainnet for safety. Switch to testnet." | |
} | |
if self.current_network != "local_testnet": | |
return { | |
"success": False, | |
"error": "Contract deployment only available on local testnet for safety" | |
} | |
# Determine if from_account is a name or address | |
private_key = None | |
from_address = None | |
if validate_ethereum_address(from_account): | |
# It's an address - check if we have the private key for it | |
from_address = Web3.to_checksum_address(from_account) | |
# Look for this address in our accounts | |
account_found = False | |
for account_name, account_data in self.accounts.items(): | |
if account_data["address"].lower() == from_address.lower(): | |
private_key = account_data["private_key"] | |
account_found = True | |
break | |
if not account_found: | |
return { | |
"success": False, | |
"error": f"Private key not available for address '{from_address}'. Use an account created in this session." | |
} | |
else: | |
# It's an account name | |
if from_account not in self.accounts: | |
available_accounts = list(self.accounts.keys()) | |
return { | |
"success": False, | |
"error": f"Account '{from_account}' not found. Available accounts: {available_accounts}" | |
} | |
account_data = self.accounts[from_account] | |
private_key = account_data["private_key"] | |
from_address = account_data["address"] | |
# Compile the contract | |
try: | |
compiled_sol = compile_source(source_code) | |
contract_interface = None | |
# Find the contract in compiled output | |
for contract_id, contract_data in compiled_sol.items(): | |
if contract_name in contract_id: | |
contract_interface = contract_data | |
break | |
if not contract_interface: | |
return {"success": False, "error": f"Contract '{contract_name}' not found in source code"} | |
except Exception as e: | |
return {"success": False, "error": f"Compilation failed: {str(e)}"} | |
# # Get account details | |
# account_data = self.accounts[from_account] | |
# private_key = account_data["private_key"] | |
# from_address = account_data["address"] | |
# Verify we have the necessary account details | |
if not private_key or not from_address: | |
return {"success": False, "error": "Unable to retrieve account details for deployment"} | |
# Create contract instance | |
contract = self.w3.eth.contract( | |
abi=contract_interface['abi'], | |
bytecode=contract_interface['bin'] | |
) | |
# Prepare constructor arguments | |
constructor_args = constructor_args or [] | |
# Check balance before deployment | |
balance_wei = self.w3.eth.get_balance(from_address) | |
if balance_wei == 0: | |
return { | |
"success": False, | |
"error": f"Account {from_address} has zero balance. Cannot deploy contract." | |
} | |
# Build deployment transaction | |
nonce = self.w3.eth.get_transaction_count(from_address) | |
# Estimate gas for deployment | |
try: | |
gas_estimate = contract.constructor(*constructor_args).estimate_gas({ | |
'from': from_address | |
}) | |
# Add 20% buffer to gas estimate | |
gas_limit = int(gas_estimate * 1.2) | |
except Exception as e: | |
logger.warning(f"Gas estimation failed: {e}") | |
gas_limit = 3000000 # Default gas limit | |
# Check if account has enough gas | |
gas_cost = gas_limit * self.w3.eth.gas_price | |
if balance_wei < gas_cost: | |
return { | |
"success": False, | |
"error": f"Insufficient balance for gas. Required: {self.w3.from_wei(gas_cost, 'ether')} ETH, Available: {self.w3.from_wei(balance_wei, 'ether')} ETH" | |
} | |
# Build and sign transaction | |
transaction = contract.constructor(*constructor_args).build_transaction({ | |
'from': from_address, | |
'gas': gas_limit, | |
'gasPrice': self.w3.eth.gas_price, | |
'nonce': nonce, | |
}) | |
signed_txn = self.w3.eth.account.sign_transaction(transaction, private_key) | |
tx_hash = self.w3.eth.send_raw_transaction(signed_txn.raw_transaction) | |
# Wait for transaction receipt | |
tx_receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash) | |
if tx_receipt.status == 1: | |
return { | |
"success": True, | |
"contract_address": tx_receipt.contractAddress, | |
"transaction_hash": tx_hash.hex(), | |
"gas_used": tx_receipt.gasUsed, | |
"gas_cost_eth": str(self.w3.from_wei(tx_receipt.gasUsed * self.w3.eth.gas_price, 'ether')), | |
"contract_name": contract_name, | |
"deployed_by": from_address, | |
"deployer_input": from_account, | |
"network": self.current_network, | |
"abi": contract_interface['abi'] | |
} | |
else: | |
return { | |
"success": False, | |
"error": "Contract deployment failed", | |
"transaction_hash": tx_hash.hex() | |
} | |
except Exception as e: | |
logger.error(f"Contract deployment error: {e}") | |
return {"success": False, "error": str(e)} | |
# Initialize the Ethereum client | |
config = EthereumConfig(use_testnet=True) | |
eth_client = EthereumClient(config) | |
# MCP Tool Functions | |
def switch_ethereum_network(network: str, api_key: str = None) -> dict: | |
""" | |
Switch between different Ethereum networks. | |
Args: | |
network (str): Network to switch to ('local_testnet', 'ethereum_mainnet', 'ethereum_sepolia') | |
api_key (str): API key for Infura or Alchemy (required for mainnet/sepolia) | |
Returns: | |
dict: Network switch result | |
""" | |
return eth_client.switch_network(network, api_key) | |
def get_network_info() -> dict: | |
""" | |
Get current network information. | |
Returns: | |
dict: Current network details | |
""" | |
try: | |
if not eth_client.w3 or not eth_client.w3.is_connected(): | |
return {"success": False, "error": "Not connected to any network"} | |
latest_block = eth_client.w3.eth.block_number | |
chain_id = eth_client.w3.eth.chain_id if hasattr(eth_client.w3.eth, 'chain_id') else None | |
return { | |
"success": True, | |
"network": eth_client.current_network, | |
"connected": eth_client.w3.is_connected(), | |
"latest_block": latest_block, | |
"chain_id": chain_id, | |
"readonly_mode": eth_client.is_mainnet_readonly | |
} | |
except Exception as e: | |
return {"success": False, "error": str(e)} | |
def create_ethereum_account(name: str) -> dict: | |
""" | |
Create a new Ethereum account with automatic funding on testnet. | |
Note: Only works on local testnet for safety. | |
Args: | |
name (str): Name for the new account | |
Returns: | |
dict: Account creation result | |
""" | |
try: | |
if eth_client.current_network != "local_testnet": | |
return { | |
"success": False, | |
"error": "Account creation only available on local testnet for safety" | |
} | |
account = Account.create() | |
# Fund the new account from test accounts | |
test_accounts = eth_client.eth_tester.get_accounts() | |
if test_accounts: | |
tx_hash = eth_client.w3.eth.send_transaction({ | |
'from': test_accounts[0], | |
'to': account.address, | |
'value': eth_client.w3.to_wei(10, 'ether'), | |
'gas': 21000, | |
'gasPrice': eth_client.w3.to_wei(20, 'gwei') | |
}) | |
eth_client.accounts[name] = { | |
"address": account.address, | |
"private_key": account.key.hex() | |
} | |
return { | |
"success": True, | |
"name": name, | |
"address": account.address, | |
"balance_eth": "10.0", | |
"funded": True, | |
"transaction_hash": tx_hash.hex(), | |
"network": eth_client.current_network | |
} | |
except Exception as e: | |
return {"success": False, "error": str(e)} | |
def get_ethereum_balance(address: str) -> dict: | |
""" | |
Get ETH balance for an Ethereum address. | |
Works on all networks. | |
Args: | |
address (str): Ethereum address to check balance for | |
Returns: | |
dict: Balance information | |
""" | |
try: | |
if not eth_client.w3 or not eth_client.w3.is_connected(): | |
return {"success": False, "error": "Not connected to any network"} | |
balance_wei = eth_client.w3.eth.get_balance(address) | |
balance_eth = eth_client.w3.from_wei(balance_wei, 'ether') | |
return { | |
"success": True, | |
"address": address, | |
"balance_wei": str(balance_wei), | |
"balance_eth": str(balance_eth), | |
"network": eth_client.current_network | |
} | |
except Exception as e: | |
return {"success": False, "error": str(e)} | |
def send_ethereum_transaction(from_account: str, to_address: str, amount_eth: float) -> dict: | |
""" | |
Send ETH from one account to another. | |
Note: Only works on local testnet for safety. | |
Args: | |
from_account (str): Name of the account to send from | |
to_address (str): Ethereum address to send to | |
amount_eth (float): Amount of ETH to send | |
Returns: | |
dict: Transaction result | |
""" | |
try: | |
if eth_client.is_mainnet_readonly: | |
return { | |
"success": False, | |
"error": "Transactions disabled on mainnet for safety. Switch to testnet for transactions." | |
} | |
if eth_client.current_network != "local_testnet": | |
return { | |
"success": False, | |
"error": "Transactions only available on local testnet for safety" | |
} | |
if from_account not in eth_client.accounts: | |
return {"success": False, "error": "Account not found"} | |
account_data = eth_client.accounts[from_account] | |
private_key = account_data["private_key"] | |
from_address = account_data["address"] | |
# Build and send transaction | |
nonce = eth_client.w3.eth.get_transaction_count(from_address) | |
amount_wei = eth_client.w3.to_wei(amount_eth, 'ether') | |
transaction = { | |
'to': to_address, | |
'value': amount_wei, | |
'gas': 21000, | |
'gasPrice': eth_client.w3.eth.gas_price, | |
'nonce': nonce, | |
} | |
signed_txn = eth_client.w3.eth.account.sign_transaction(transaction, private_key) | |
tx_hash = eth_client.w3.eth.send_raw_transaction(signed_txn.raw_transaction) | |
return { | |
"success": True, | |
"transaction_hash": tx_hash.hex(), | |
"from": from_address, | |
"to": to_address, | |
"amount_eth": amount_eth, | |
"network": eth_client.current_network | |
} | |
except Exception as e: | |
return {"success": False, "error": str(e)} | |
def get_latest_ethereum_block() -> dict: | |
""" | |
Get information about the latest block. | |
Works on all networks. | |
Returns: | |
dict: Latest block information | |
""" | |
try: | |
if not eth_client.w3 or not eth_client.w3.is_connected(): | |
return {"success": False, "error": "Not connected to any network"} | |
block = eth_client.w3.eth.get_block("latest") | |
return { | |
"success": True, | |
"block_number": block.number, | |
"block_hash": block.hash.hex(), | |
"timestamp": block.timestamp, | |
"transaction_count": len(block.transactions), | |
"gas_used": block.gasUsed, | |
"gas_limit": block.gasLimit, | |
"network": eth_client.current_network | |
} | |
except Exception as e: | |
return {"success": False, "error": str(e)} | |
def get_transaction_info(tx_hash: str) -> dict: | |
""" | |
Get information about a specific transaction. | |
Works on all networks. | |
Args: | |
tx_hash (str): Transaction hash to look up | |
Returns: | |
dict: Transaction information | |
""" | |
try: | |
if not eth_client.w3 or not eth_client.w3.is_connected(): | |
return {"success": False, "error": "Not connected to any network"} | |
tx = eth_client.w3.eth.get_transaction(tx_hash) | |
tx_receipt = eth_client.w3.eth.get_transaction_receipt(tx_hash) | |
return { | |
"success": True, | |
"hash": tx.hash.hex(), | |
"from": tx["from"], | |
"to": tx.to, | |
"value_eth": str(eth_client.w3.from_wei(tx.value, 'ether')), | |
"gas": tx.gas, | |
"gas_price": tx.gasPrice, | |
"block_number": tx.blockNumber, | |
"status": tx_receipt.status, | |
"gas_used": tx_receipt.gasUsed, | |
"network": eth_client.current_network | |
} | |
except Exception as e: | |
return {"success": False, "error": str(e)} | |
def list_ethereum_accounts() -> dict: | |
""" | |
List all created Ethereum accounts with their balances. | |
Note: Only shows local testnet accounts for safety. | |
Returns: | |
dict: List of accounts | |
""" | |
try: | |
if eth_client.current_network != "local_testnet": | |
return { | |
"success": True, | |
"accounts": [], | |
"total_accounts": 0, | |
"message": "Account list only available on local testnet for safety" | |
} | |
accounts_info = [] | |
for name, data in eth_client.accounts.items(): | |
balance = get_ethereum_balance(data['address']) | |
accounts_info.append({ | |
"name": name, | |
"address": data['address'], | |
"balance_eth": balance.get('balance_eth', '0') if balance.get('success') else '0' | |
}) | |
return { | |
"success": True, | |
"accounts": accounts_info, | |
"total_accounts": len(accounts_info), | |
"network": eth_client.current_network | |
} | |
except Exception as e: | |
return {"success": False, "error": str(e)} | |
def mine_ethereum_blocks(num_blocks: int = 1) -> dict: | |
""" | |
Mine blocks on the local testnet (for testing purposes). | |
Note: Only works on local testnet. | |
Args: | |
num_blocks (int): Number of blocks to mine | |
Returns: | |
dict: Mining result | |
""" | |
try: | |
if eth_client.current_network != "local_testnet" or not eth_client.eth_tester: | |
return { | |
"success": False, | |
"error": "Block mining only available on local testnet" | |
} | |
initial_block = eth_client.w3.eth.block_number | |
for _ in range(num_blocks): | |
eth_client.eth_tester.mine_block() | |
final_block = eth_client.w3.eth.block_number | |
return { | |
"success": True, | |
"initial_block": initial_block, | |
"final_block": final_block, | |
"blocks_mined": num_blocks, | |
"network": eth_client.current_network | |
} | |
except Exception as e: | |
return {"success": False, "error": str(e)} | |
def deploy_smart_contract(source_code: str, contract_name: str, from_account: str, constructor_args: list = None) -> dict: | |
""" | |
Compile and deploy a smart contract to the blockchain. | |
Note: Only works on local testnet for safety. | |
Args: | |
source_code (str): Solidity source code | |
contract_name (str): Name of the contract to deploy | |
from_account (str): Name of the account or eth address to deploy from | |
constructor_args (list): Constructor arguments if any | |
Returns: | |
dict: Contract deployment result | |
""" | |
return eth_client.compile_and_deploy_contract(source_code, contract_name, from_account, constructor_args) | |
# Gradio Interface Functions | |
def switch_network_ui(network: str, api_key: str): | |
"""Gradio wrapper for network switching""" | |
result = switch_ethereum_network(network, api_key if api_key.strip() else None) | |
return json.dumps(result, indent=2) | |
def get_network_info_ui(): | |
"""Get current network info for UI""" | |
result = get_network_info() | |
if result.get('success'): | |
info = f"Network: {result['network']}\n" | |
info += f"Connected: {result['connected']}\n" | |
info += f"Latest Block: {result['latest_block']}\n" | |
info += f"Chain ID: {result.get('chain_id', 'N/A')}\n" | |
info += f"Read-Only Mode: {result['readonly_mode']}" | |
return info | |
return f"Error: {result.get('error', 'Unknown error')}" | |
def create_account_ui(name: str): | |
"""Gradio wrapper for create account""" | |
if not name: | |
return "Please provide an account name" | |
result = create_ethereum_account(name) | |
return json.dumps(result, indent=2) | |
def get_balance_ui(address: str): | |
"""Gradio wrapper for get balance""" | |
if not address: | |
return "Please provide an address" | |
result = get_ethereum_balance(address) | |
return json.dumps(result, indent=2) | |
def send_transaction_ui(from_account: str, to_address: str, amount_eth: float): | |
"""Gradio wrapper for send transaction""" | |
if not from_account or not to_address or amount_eth <= 0: | |
return "Please provide valid transaction details" | |
result = send_ethereum_transaction(from_account, to_address, amount_eth) | |
return json.dumps(result, indent=2) | |
def get_tx_info_ui(tx_hash: str): | |
"""Gradio wrapper for transaction info""" | |
if not tx_hash: | |
return "Please provide a transaction hash" | |
result = get_transaction_info(tx_hash) | |
return json.dumps(result, indent=2) | |
def get_accounts_list(): | |
"""Get list of current accounts with balances""" | |
result = list_ethereum_accounts() | |
if result.get('success'): | |
accounts = result.get('accounts', []) | |
if accounts: | |
account_list = [] | |
for acc in accounts: | |
account_list.append(f"{acc['name']}: {acc['address']} ({acc['balance_eth']} ETH)") | |
return "\n".join(account_list) | |
else: | |
return result.get('message', 'No accounts available') | |
return f"Error: {result.get('error', 'Unknown error')}" | |
def mine_blocks_ui(num_blocks: int): | |
"""Mine blocks UI wrapper""" | |
if num_blocks <= 0: | |
return "Please provide a positive number of blocks" | |
result = mine_ethereum_blocks(num_blocks) | |
return json.dumps(result, indent=2) | |
def deploy_contract_ui(source_code: str, contract_name: str, from_account: str, constructor_args_str: str): | |
"""Gradio wrapper for contract deployment""" | |
if not source_code or not contract_name or not from_account: | |
return "Please provide source code, contract name, and from account or address" | |
# Parse constructor arguments if provided | |
constructor_args = [] | |
if constructor_args_str.strip(): | |
try: | |
# Parse constructor arguments with type conversion | |
args_list = [arg.strip() for arg in constructor_args_str.split(',') if arg.strip()] | |
for arg in args_list: | |
# Try to convert to appropriate types | |
if arg.lower() in ['true', 'false']: | |
# Boolean | |
constructor_args.append(arg.lower() == 'true') | |
elif arg.startswith('0x') and len(arg) == 42: | |
# Ethereum address | |
constructor_args.append(to_checksum_address(arg)) | |
elif arg.startswith('"') and arg.endswith('"'): | |
# String (remove quotes) | |
constructor_args.append(arg[1:-1]) | |
elif arg.startswith("'") and arg.endswith("'"): | |
# String (remove quotes) | |
constructor_args.append(arg[1:-1]) | |
elif '.' in arg: | |
# Float/Decimal - convert to int if it's actually a whole number | |
try: | |
float_val = float(arg) | |
if float_val.is_integer(): | |
constructor_args.append(int(float_val)) | |
else: | |
constructor_args.append(float_val) | |
except ValueError: | |
constructor_args.append(arg) # Keep as string if conversion fails | |
else: | |
# Try integer first, then keep as string | |
try: | |
constructor_args.append(int(arg)) | |
except ValueError: | |
constructor_args.append(arg) | |
except Exception as e: | |
return f"Error parsing constructor arguments: {str(e)}" | |
result = deploy_smart_contract(source_code, contract_name, from_account, constructor_args) | |
return json.dumps(result, indent=2) | |
# Create Gradio Interface | |
def create_gradio_app(): | |
"""Create the Gradio web interface""" | |
with gr.Blocks(title="Ethereum MCP Server", theme=gr.themes.Soft()) as app: | |
gr.Markdown("# βοΈ Ethereum MCP Server ") | |
gr.Markdown("π **Safety** - Mainnet is read-only, transactions only on local testnet") | |
with gr.Tabs(): | |
# Network Management Tab | |
with gr.Tab("π Network Management"): | |
gr.Markdown("### Switch Ethereum Networks") | |
gr.Markdown("**Available Networks:**") | |
gr.Markdown("- **Local TestNet**: Full functionality, safe for testing") | |
gr.Markdown("- **Ethereum Mainnet**: Read-only access (balance checks, block info)") | |
gr.Markdown("- **Ethereum Sepolia**: Testnet with real network conditions") | |
with gr.Row(): | |
with gr.Column(): | |
network_choice = gr.Radio( | |
choices=["local_testnet", "ethereum_mainnet", "ethereum_sepolia"], | |
value="local_testnet", | |
label="Select Network" | |
) | |
api_key_input = gr.Textbox( | |
label="API Key (Infura/Alchemy)", | |
placeholder="Required for mainnet/sepolia", | |
type="password" | |
) | |
switch_btn = gr.Button("Switch Network", variant="primary") | |
switch_output = gr.Textbox(label="Result", lines=8) | |
switch_btn.click( | |
switch_network_ui, | |
inputs=[network_choice, api_key_input], | |
outputs=[switch_output] | |
) | |
with gr.Column(): | |
gr.Markdown("#### Current Network Status") | |
network_info = gr.Textbox(label="Network Info", lines=6, interactive=False) | |
refresh_network = gr.Button("Refresh Network Info") | |
refresh_network.click( | |
get_network_info_ui, | |
outputs=[network_info] | |
) | |
# Account Management Tab | |
with gr.Tab("π€ Account Management"): | |
gr.Markdown("### Ethereum Accounts") | |
gr.Markdown("β οΈ **Safety Note**: Account creation only available on local testnet") | |
with gr.Row(): | |
with gr.Column(): | |
gr.Markdown("#### Create New Account") | |
create_name = gr.Textbox(label="Account Name", placeholder="my-account") | |
create_btn = gr.Button("Create Account", variant="primary") | |
create_output = gr.Textbox(label="Result", lines=10) | |
create_btn.click( | |
create_account_ui, | |
inputs=[create_name], | |
outputs=[create_output] | |
) | |
gr.Markdown("#### Current Accounts") | |
accounts_display = gr.Textbox(label="Accounts", lines=5, interactive=False) | |
refresh_accounts = gr.Button("Refresh Accounts") | |
refresh_accounts.click( | |
get_accounts_list, | |
outputs=[accounts_display] | |
) | |
# Balance & Transactions Tab | |
with gr.Tab("π° Balance & Transactions"): | |
with gr.Row(): | |
with gr.Column(): | |
gr.Markdown("#### Check Balance") | |
gr.Markdown("*Works on all networks*") | |
balance_address = gr.Textbox( | |
label="Address", | |
placeholder="0x... (works with any Ethereum address)" | |
) | |
balance_btn = gr.Button("Get Balance", variant="primary") | |
balance_output = gr.Textbox(label="Balance Info", lines=8) | |
balance_btn.click( | |
get_balance_ui, | |
inputs=[balance_address], | |
outputs=[balance_output] | |
) | |
gr.Markdown("#### Transaction Info") | |
tx_hash_input = gr.Textbox(label="Transaction Hash", placeholder="0x...") | |
tx_info_btn = gr.Button("Get Transaction Info") | |
tx_info_output = gr.Textbox(label="Transaction Details", lines=8) | |
tx_info_btn.click( | |
get_tx_info_ui, | |
inputs=[tx_hash_input], | |
outputs=[tx_info_output] | |
) | |
with gr.Column(): | |
gr.Markdown("#### Send Transaction") | |
gr.Markdown("*β οΈ Only available on local testnet for safety*") | |
tx_from = gr.Textbox(label="From Account Name", placeholder="my-account") | |
tx_to = gr.Textbox(label="To Address", placeholder="0x...") | |
tx_amount = gr.Number(label="Amount (ETH)", minimum=0, step=0.001) | |
tx_btn = gr.Button("Send Transaction", variant="primary") | |
tx_output = gr.Textbox(label="Transaction Result", lines=8) | |
tx_btn.click( | |
send_transaction_ui, | |
inputs=[tx_from, tx_to, tx_amount], | |
outputs=[tx_output] | |
) | |
# Blockchain Info Tab | |
with gr.Tab("π Blockchain Info"): | |
gr.Markdown("### Blockchain Information") | |
gr.Markdown("*Available on all networks*") | |
with gr.Row(): | |
with gr.Column(): | |
gr.Markdown("#### Latest Block Info") | |
latest_block_btn = gr.Button("Get Latest Block", variant="primary") | |
latest_block_output = gr.JSON(label="Latest Block") | |
latest_block_btn.click( | |
fn=get_latest_ethereum_block, | |
outputs=latest_block_output | |
) | |
gr.Markdown("#### Mine Blocks (Local TestNet Only)") | |
mine_blocks_input = gr.Number(label="Number of Blocks", value=1, minimum=1, step=1) | |
mine_btn = gr.Button("Mine Blocks") | |
mine_output = gr.Textbox(label="Mining Result", lines=5) | |
mine_btn.click( | |
mine_blocks_ui, | |
inputs=[mine_blocks_input], | |
outputs=[mine_output] | |
) | |
with gr.Column(): | |
gr.Markdown("#### All Accounts Info") | |
list_accounts_btn = gr.Button("List All Accounts") | |
accounts_json_output = gr.JSON(label="All Accounts") | |
list_accounts_btn.click( | |
fn=list_ethereum_accounts, | |
outputs=accounts_json_output | |
) | |
with gr.Tab("π Smart Contracts"): | |
gr.Markdown("### Deploy Smart Contracts") | |
gr.Markdown("*β οΈ Only available on local testnet for safety*") | |
with gr.Row(): | |
with gr.Column(): | |
gr.Markdown("#### Contract Deployment") | |
contract_source = gr.Textbox( | |
label="Solidity Source Code", | |
placeholder="""pragma solidity ^0.8.19; | |
contract SimpleStorage { | |
uint256 public storedData; | |
constructor(uint256 _initialValue) { | |
storedData = _initialValue; | |
} | |
function set(uint256 _value) public { | |
storedData = _value; | |
} | |
function get() public view returns (uint256) { | |
return storedData; | |
} | |
}""", | |
lines=15 | |
) | |
with gr.Row(): | |
contract_name_input = gr.Textbox( | |
label="Contract Name", | |
placeholder="SimpleStorage" | |
) | |
deploy_from_account = gr.Textbox( | |
label="Deploy From Account", | |
placeholder="test_account_1" | |
) | |
constructor_args_input = gr.Textbox( | |
label="Constructor Arguments (comma-separated)", | |
placeholder="100", | |
info="Examples: 100 (number), \"Hello\" (string), 0x1234...abcd (address), true/false (boolean)" | |
) | |
deploy_btn = gr.Button("Deploy Contract", variant="primary") | |
deploy_output = gr.Textbox(label="Deployment Result", lines=12) | |
deploy_btn.click( | |
deploy_contract_ui, | |
inputs=[contract_source, contract_name_input, deploy_from_account, constructor_args_input], | |
outputs=[deploy_output] | |
) | |
with gr.Column(): | |
gr.Markdown("#### Example Contracts") | |
gr.Markdown(""" | |
**Simple Storage Contract:** | |
```solidity | |
pragma solidity ^0.8.19; | |
contract SimpleStorage { | |
uint256 public storedData; | |
constructor(uint256 _initialValue) { | |
storedData = _initialValue; | |
} | |
function set(uint256 _value) public { | |
storedData = _value; | |
} | |
}""") | |
gr.Markdown("---") | |
gr.Markdown("### π Safety Features") | |
gr.Markdown("- **Mainnet Read-Only**: No transactions on mainnet, only balance/block queries") | |
gr.Markdown("- **No Private Key Storage**: Keys only exist in local testnet memory") | |
gr.Markdown("- **API Key Security**: Keys entered are not logged or stored") | |
gr.Markdown("### π€ MCP Integration") | |
gr.Markdown("**Available MCP Tools**: `switch_ethereum_network`, `get_network_info`, `create_ethereum_account`, `get_ethereum_balance`, `send_ethereum_transaction`, `get_latest_ethereum_block`, `get_transaction_info`, `list_ethereum_accounts`, `mine_ethereum_blocks`, `deploy_smart_contract`") | |
return app | |
# Launch the interface | |
if __name__ == "__main__": | |
print("π Starting Enhanced Ethereum MCP Server...") | |
print(f"π Network: {eth_client.current_network}") | |
print(f"β Connected: {eth_client.w3.is_connected()}") | |
print(f"π§± Latest block: {eth_client.w3.eth.block_number}") | |
print("π Prototype - mainnet is read-only for security reason") | |
# Create and launch the app | |
demo = create_gradio_app() | |
demo.launch( | |
server_name="0.0.0.0", | |
server_port=7860, | |
share=False, | |
show_error=True, | |
mcp_server=True | |
) |