Spaces:
Sleeping
Sleeping
Sarah Azouvi commited on
Commit ·
37f012e
1
Parent(s): 68e14c4
added smart contract
Browse files- app.py +261 -2
- requirements.txt +2 -0
app.py
CHANGED
|
@@ -14,6 +14,9 @@ from web3 import Web3
|
|
| 14 |
from eth_tester import EthereumTester
|
| 15 |
from web3.providers.eth_tester import EthereumTesterProvider
|
| 16 |
from eth_account import Account
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
# Configure logging
|
| 19 |
logging.basicConfig(level=logging.INFO)
|
|
@@ -40,6 +43,14 @@ class EthereumClient:
|
|
| 40 |
|
| 41 |
# Initialize with local testnet by default
|
| 42 |
self._connect_local_testnet()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
def _connect_local_testnet(self):
|
| 45 |
"""Connect to local test blockchain"""
|
|
@@ -179,6 +190,116 @@ class EthereumClient:
|
|
| 179 |
except Exception as e:
|
| 180 |
logger.error(f"Error funding test account: {e}")
|
| 181 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
# Initialize the Ethereum client
|
| 183 |
config = EthereumConfig(use_testnet=True)
|
| 184 |
eth_client = EthereumClient(config)
|
|
@@ -343,7 +464,7 @@ def send_ethereum_transaction(from_account: str, to_address: str, amount_eth: fl
|
|
| 343 |
}
|
| 344 |
|
| 345 |
signed_txn = eth_client.w3.eth.account.sign_transaction(transaction, private_key)
|
| 346 |
-
tx_hash = eth_client.w3.eth.send_raw_transaction(signed_txn.
|
| 347 |
|
| 348 |
return {
|
| 349 |
"success": True,
|
|
@@ -486,6 +607,23 @@ def mine_ethereum_blocks(num_blocks: int = 1) -> dict:
|
|
| 486 |
except Exception as e:
|
| 487 |
return {"success": False, "error": str(e)}
|
| 488 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 489 |
# Gradio Interface Functions
|
| 490 |
def switch_network_ui(network: str, api_key: str):
|
| 491 |
"""Gradio wrapper for network switching"""
|
|
@@ -553,6 +691,55 @@ def mine_blocks_ui(num_blocks: int):
|
|
| 553 |
result = mine_ethereum_blocks(num_blocks)
|
| 554 |
return json.dumps(result, indent=2)
|
| 555 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 556 |
# Create Gradio Interface
|
| 557 |
def create_gradio_app():
|
| 558 |
"""Create the Gradio web interface"""
|
|
@@ -705,6 +892,78 @@ def create_gradio_app():
|
|
| 705 |
fn=list_ethereum_accounts,
|
| 706 |
outputs=accounts_json_output
|
| 707 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 708 |
|
| 709 |
gr.Markdown("---")
|
| 710 |
gr.Markdown("### 🔒 Safety Features for Public Deployment")
|
|
@@ -714,7 +973,7 @@ def create_gradio_app():
|
|
| 714 |
gr.Markdown("- **Transaction Restrictions**: Transactions only available on safe local testnet")
|
| 715 |
|
| 716 |
gr.Markdown("### 🤖 MCP Integration")
|
| 717 |
-
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`")
|
| 718 |
|
| 719 |
return app
|
| 720 |
|
|
|
|
| 14 |
from eth_tester import EthereumTester
|
| 15 |
from web3.providers.eth_tester import EthereumTesterProvider
|
| 16 |
from eth_account import Account
|
| 17 |
+
from solcx import compile_source, install_solc
|
| 18 |
+
import solcx
|
| 19 |
+
from eth_utils import to_checksum_address
|
| 20 |
|
| 21 |
# Configure logging
|
| 22 |
logging.basicConfig(level=logging.INFO)
|
|
|
|
| 43 |
|
| 44 |
# Initialize with local testnet by default
|
| 45 |
self._connect_local_testnet()
|
| 46 |
+
|
| 47 |
+
# Install Solidity compiler for contract compilation
|
| 48 |
+
try:
|
| 49 |
+
install_solc('0.8.19')
|
| 50 |
+
solcx.set_solc_version('0.8.19')
|
| 51 |
+
except Exception as e:
|
| 52 |
+
logger.warning(f"Could not install Solidity compiler: {e}")
|
| 53 |
+
|
| 54 |
|
| 55 |
def _connect_local_testnet(self):
|
| 56 |
"""Connect to local test blockchain"""
|
|
|
|
| 190 |
except Exception as e:
|
| 191 |
logger.error(f"Error funding test account: {e}")
|
| 192 |
|
| 193 |
+
def compile_and_deploy_contract(self, source_code: str, contract_name: str, from_account: str, constructor_args: list = None) -> dict:
|
| 194 |
+
"""
|
| 195 |
+
Compile and deploy a smart contract.
|
| 196 |
+
Only works on local testnet for safety.
|
| 197 |
+
|
| 198 |
+
Args:
|
| 199 |
+
source_code (str): Solidity source code
|
| 200 |
+
contract_name (str): Name of the contract to deploy
|
| 201 |
+
from_account (str): Account name to deploy from
|
| 202 |
+
constructor_args (list): Constructor arguments if any
|
| 203 |
+
|
| 204 |
+
Returns:
|
| 205 |
+
dict: Deployment result
|
| 206 |
+
"""
|
| 207 |
+
try:
|
| 208 |
+
if self.is_mainnet_readonly:
|
| 209 |
+
return {
|
| 210 |
+
"success": False,
|
| 211 |
+
"error": "Contract deployment disabled on mainnet for safety. Switch to testnet."
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
if self.current_network != "local_testnet":
|
| 215 |
+
return {
|
| 216 |
+
"success": False,
|
| 217 |
+
"error": "Contract deployment only available on local testnet for safety in public deployment"
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
if from_account not in self.accounts:
|
| 221 |
+
return {"success": False, "error": "Account not found"}
|
| 222 |
+
|
| 223 |
+
# Compile the contract
|
| 224 |
+
try:
|
| 225 |
+
compiled_sol = compile_source(source_code)
|
| 226 |
+
contract_interface = None
|
| 227 |
+
|
| 228 |
+
# Find the contract in compiled output
|
| 229 |
+
for contract_id, contract_data in compiled_sol.items():
|
| 230 |
+
if contract_name in contract_id:
|
| 231 |
+
contract_interface = contract_data
|
| 232 |
+
break
|
| 233 |
+
|
| 234 |
+
if not contract_interface:
|
| 235 |
+
return {"success": False, "error": f"Contract '{contract_name}' not found in source code"}
|
| 236 |
+
|
| 237 |
+
except Exception as e:
|
| 238 |
+
return {"success": False, "error": f"Compilation failed: {str(e)}"}
|
| 239 |
+
|
| 240 |
+
# Get account details
|
| 241 |
+
account_data = self.accounts[from_account]
|
| 242 |
+
private_key = account_data["private_key"]
|
| 243 |
+
from_address = account_data["address"]
|
| 244 |
+
|
| 245 |
+
# Create contract instance
|
| 246 |
+
contract = self.w3.eth.contract(
|
| 247 |
+
abi=contract_interface['abi'],
|
| 248 |
+
bytecode=contract_interface['bin']
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
# Prepare constructor arguments
|
| 252 |
+
constructor_args = constructor_args or []
|
| 253 |
+
|
| 254 |
+
# Build deployment transaction
|
| 255 |
+
nonce = self.w3.eth.get_transaction_count(from_address)
|
| 256 |
+
|
| 257 |
+
# Estimate gas for deployment
|
| 258 |
+
try:
|
| 259 |
+
gas_estimate = contract.constructor(*constructor_args).estimate_gas({
|
| 260 |
+
'from': from_address
|
| 261 |
+
})
|
| 262 |
+
# Add 20% buffer to gas estimate
|
| 263 |
+
gas_limit = int(gas_estimate * 1.2)
|
| 264 |
+
except Exception as e:
|
| 265 |
+
gas_limit = 3000000 # Default gas limit
|
| 266 |
+
|
| 267 |
+
# Build and sign transaction
|
| 268 |
+
transaction = contract.constructor(*constructor_args).build_transaction({
|
| 269 |
+
'from': from_address,
|
| 270 |
+
'gas': gas_limit,
|
| 271 |
+
'gasPrice': self.w3.eth.gas_price,
|
| 272 |
+
'nonce': nonce,
|
| 273 |
+
})
|
| 274 |
+
|
| 275 |
+
signed_txn = self.w3.eth.account.sign_transaction(transaction, private_key)
|
| 276 |
+
tx_hash = self.w3.eth.send_raw_transaction(signed_txn.raw_transaction)
|
| 277 |
+
|
| 278 |
+
# Wait for transaction receipt
|
| 279 |
+
tx_receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash)
|
| 280 |
+
|
| 281 |
+
if tx_receipt.status == 1:
|
| 282 |
+
return {
|
| 283 |
+
"success": True,
|
| 284 |
+
"contract_address": tx_receipt.contractAddress,
|
| 285 |
+
"transaction_hash": tx_hash.hex(),
|
| 286 |
+
"gas_used": tx_receipt.gasUsed,
|
| 287 |
+
"contract_name": contract_name,
|
| 288 |
+
"deployed_by": from_address,
|
| 289 |
+
"network": self.current_network,
|
| 290 |
+
"abi": contract_interface['abi']
|
| 291 |
+
}
|
| 292 |
+
else:
|
| 293 |
+
return {
|
| 294 |
+
"success": False,
|
| 295 |
+
"error": "Contract deployment failed",
|
| 296 |
+
"transaction_hash": tx_hash.hex()
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
except Exception as e:
|
| 300 |
+
return {"success": False, "error": str(e)}
|
| 301 |
+
|
| 302 |
+
|
| 303 |
# Initialize the Ethereum client
|
| 304 |
config = EthereumConfig(use_testnet=True)
|
| 305 |
eth_client = EthereumClient(config)
|
|
|
|
| 464 |
}
|
| 465 |
|
| 466 |
signed_txn = eth_client.w3.eth.account.sign_transaction(transaction, private_key)
|
| 467 |
+
tx_hash = eth_client.w3.eth.send_raw_transaction(signed_txn.raw_transaction)
|
| 468 |
|
| 469 |
return {
|
| 470 |
"success": True,
|
|
|
|
| 607 |
except Exception as e:
|
| 608 |
return {"success": False, "error": str(e)}
|
| 609 |
|
| 610 |
+
def deploy_smart_contract(source_code: str, contract_name: str, from_account: str, constructor_args: list = None) -> dict:
|
| 611 |
+
"""
|
| 612 |
+
Compile and deploy a smart contract to the blockchain.
|
| 613 |
+
Note: Only works on local testnet for safety.
|
| 614 |
+
|
| 615 |
+
Args:
|
| 616 |
+
source_code (str): Solidity source code
|
| 617 |
+
contract_name (str): Name of the contract to deploy
|
| 618 |
+
from_account (str): Name of the account to deploy from
|
| 619 |
+
constructor_args (list): Constructor arguments if any
|
| 620 |
+
|
| 621 |
+
Returns:
|
| 622 |
+
dict: Contract deployment result
|
| 623 |
+
"""
|
| 624 |
+
return eth_client.compile_and_deploy_contract(source_code, contract_name, from_account, constructor_args)
|
| 625 |
+
|
| 626 |
+
|
| 627 |
# Gradio Interface Functions
|
| 628 |
def switch_network_ui(network: str, api_key: str):
|
| 629 |
"""Gradio wrapper for network switching"""
|
|
|
|
| 691 |
result = mine_ethereum_blocks(num_blocks)
|
| 692 |
return json.dumps(result, indent=2)
|
| 693 |
|
| 694 |
+
def deploy_contract_ui(source_code: str, contract_name: str, from_account: str, constructor_args_str: str):
|
| 695 |
+
"""Gradio wrapper for contract deployment"""
|
| 696 |
+
if not source_code or not contract_name or not from_account:
|
| 697 |
+
return "Please provide source code, contract name, and from account"
|
| 698 |
+
|
| 699 |
+
# Parse constructor arguments if provided
|
| 700 |
+
constructor_args = []
|
| 701 |
+
if constructor_args_str.strip():
|
| 702 |
+
try:
|
| 703 |
+
# Parse constructor arguments with type conversion
|
| 704 |
+
args_list = [arg.strip() for arg in constructor_args_str.split(',') if arg.strip()]
|
| 705 |
+
|
| 706 |
+
for arg in args_list:
|
| 707 |
+
# Try to convert to appropriate types
|
| 708 |
+
if arg.lower() in ['true', 'false']:
|
| 709 |
+
# Boolean
|
| 710 |
+
constructor_args.append(arg.lower() == 'true')
|
| 711 |
+
elif arg.startswith('0x') and len(arg) == 42:
|
| 712 |
+
# Ethereum address
|
| 713 |
+
constructor_args.append(to_checksum_address(arg))
|
| 714 |
+
elif arg.startswith('"') and arg.endswith('"'):
|
| 715 |
+
# String (remove quotes)
|
| 716 |
+
constructor_args.append(arg[1:-1])
|
| 717 |
+
elif arg.startswith("'") and arg.endswith("'"):
|
| 718 |
+
# String (remove quotes)
|
| 719 |
+
constructor_args.append(arg[1:-1])
|
| 720 |
+
elif '.' in arg:
|
| 721 |
+
# Float/Decimal - convert to int if it's actually a whole number
|
| 722 |
+
try:
|
| 723 |
+
float_val = float(arg)
|
| 724 |
+
if float_val.is_integer():
|
| 725 |
+
constructor_args.append(int(float_val))
|
| 726 |
+
else:
|
| 727 |
+
constructor_args.append(float_val)
|
| 728 |
+
except ValueError:
|
| 729 |
+
constructor_args.append(arg) # Keep as string if conversion fails
|
| 730 |
+
else:
|
| 731 |
+
# Try integer first, then keep as string
|
| 732 |
+
try:
|
| 733 |
+
constructor_args.append(int(arg))
|
| 734 |
+
except ValueError:
|
| 735 |
+
constructor_args.append(arg)
|
| 736 |
+
|
| 737 |
+
except Exception as e:
|
| 738 |
+
return f"Error parsing constructor arguments: {str(e)}"
|
| 739 |
+
|
| 740 |
+
result = deploy_smart_contract(source_code, contract_name, from_account, constructor_args)
|
| 741 |
+
return json.dumps(result, indent=2)
|
| 742 |
+
|
| 743 |
# Create Gradio Interface
|
| 744 |
def create_gradio_app():
|
| 745 |
"""Create the Gradio web interface"""
|
|
|
|
| 892 |
fn=list_ethereum_accounts,
|
| 893 |
outputs=accounts_json_output
|
| 894 |
)
|
| 895 |
+
with gr.Tab("📄 Smart Contracts"):
|
| 896 |
+
gr.Markdown("### Deploy Smart Contracts")
|
| 897 |
+
gr.Markdown("*⚠️ Only available on local testnet for safety*")
|
| 898 |
+
|
| 899 |
+
with gr.Row():
|
| 900 |
+
with gr.Column():
|
| 901 |
+
gr.Markdown("#### Contract Deployment")
|
| 902 |
+
contract_source = gr.Textbox(
|
| 903 |
+
label="Solidity Source Code",
|
| 904 |
+
placeholder="""pragma solidity ^0.8.19;
|
| 905 |
+
|
| 906 |
+
contract SimpleStorage {
|
| 907 |
+
uint256 public storedData;
|
| 908 |
+
|
| 909 |
+
constructor(uint256 _initialValue) {
|
| 910 |
+
storedData = _initialValue;
|
| 911 |
+
}
|
| 912 |
+
|
| 913 |
+
function set(uint256 _value) public {
|
| 914 |
+
storedData = _value;
|
| 915 |
+
}
|
| 916 |
+
|
| 917 |
+
function get() public view returns (uint256) {
|
| 918 |
+
return storedData;
|
| 919 |
+
}
|
| 920 |
+
}""",
|
| 921 |
+
lines=15
|
| 922 |
+
)
|
| 923 |
+
|
| 924 |
+
with gr.Row():
|
| 925 |
+
contract_name_input = gr.Textbox(
|
| 926 |
+
label="Contract Name",
|
| 927 |
+
placeholder="SimpleStorage"
|
| 928 |
+
)
|
| 929 |
+
deploy_from_account = gr.Textbox(
|
| 930 |
+
label="Deploy From Account",
|
| 931 |
+
placeholder="test_account_1"
|
| 932 |
+
)
|
| 933 |
+
|
| 934 |
+
constructor_args_input = gr.Textbox(
|
| 935 |
+
label="Constructor Arguments (comma-separated)",
|
| 936 |
+
placeholder="100",
|
| 937 |
+
info="Examples: 100 (number), \"Hello\" (string), 0x1234...abcd (address), true/false (boolean)"
|
| 938 |
+
)
|
| 939 |
+
|
| 940 |
+
deploy_btn = gr.Button("Deploy Contract", variant="primary")
|
| 941 |
+
deploy_output = gr.Textbox(label="Deployment Result", lines=12)
|
| 942 |
+
|
| 943 |
+
deploy_btn.click(
|
| 944 |
+
deploy_contract_ui,
|
| 945 |
+
inputs=[contract_source, contract_name_input, deploy_from_account, constructor_args_input],
|
| 946 |
+
outputs=[deploy_output]
|
| 947 |
+
)
|
| 948 |
+
|
| 949 |
+
with gr.Column():
|
| 950 |
+
gr.Markdown("#### Example Contracts")
|
| 951 |
+
gr.Markdown("""
|
| 952 |
+
**Simple Storage Contract:**
|
| 953 |
+
```solidity
|
| 954 |
+
pragma solidity ^0.8.19;
|
| 955 |
+
|
| 956 |
+
contract SimpleStorage {
|
| 957 |
+
uint256 public storedData;
|
| 958 |
+
|
| 959 |
+
constructor(uint256 _initialValue) {
|
| 960 |
+
storedData = _initialValue;
|
| 961 |
+
}
|
| 962 |
+
|
| 963 |
+
function set(uint256 _value) public {
|
| 964 |
+
storedData = _value;
|
| 965 |
+
}
|
| 966 |
+
}""")
|
| 967 |
|
| 968 |
gr.Markdown("---")
|
| 969 |
gr.Markdown("### 🔒 Safety Features for Public Deployment")
|
|
|
|
| 973 |
gr.Markdown("- **Transaction Restrictions**: Transactions only available on safe local testnet")
|
| 974 |
|
| 975 |
gr.Markdown("### 🤖 MCP Integration")
|
| 976 |
+
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`")
|
| 977 |
|
| 978 |
return app
|
| 979 |
|
requirements.txt
CHANGED
|
@@ -9,3 +9,5 @@ web3>=6.15.0
|
|
| 9 |
eth-account>=0.10.0
|
| 10 |
eth-tester
|
| 11 |
py-evm
|
|
|
|
|
|
|
|
|
| 9 |
eth-account>=0.10.0
|
| 10 |
eth-tester
|
| 11 |
py-evm
|
| 12 |
+
py-solc-x
|
| 13 |
+
eth-utils
|