2798 lines
105 KiB
Python
2798 lines
105 KiB
Python
"""
|
||
Solidity Web3 Site Builder - Простой конструктор сайтов с поддержкой смарт-контрактов
|
||
"""
|
||
|
||
import tkinter as tk
|
||
from tkinter import ttk, filedialog, messagebox, scrolledtext
|
||
import os
|
||
import json
|
||
import shutil
|
||
import sys
|
||
from datetime import datetime
|
||
import webbrowser
|
||
import tempfile
|
||
import secrets
|
||
from typing import Optional, Dict, Any
|
||
|
||
# Импорты библиотек
|
||
from solcx import compile_source, install_solc, set_solc_version, get_installed_solc_versions
|
||
from web3 import Web3
|
||
from web3.exceptions import ContractLogicError, TransactionNotFound, TimeExhausted
|
||
from web3.middleware import ExtraDataToPOAMiddleware # Исправлено для web3.py 7.10.0
|
||
from eth_account import Account
|
||
from eth_account.signers.local import LocalAccount
|
||
|
||
|
||
# ================ БАЗОВЫЕ КЛАССЫ ================
|
||
|
||
class Component:
|
||
"""Базовый класс для всех компонентов"""
|
||
|
||
def __init__(self, component_id: str, comp_type: str, x: int, y: int, width: int, height: int):
|
||
self.id = component_id
|
||
self.type = comp_type
|
||
self.x = x
|
||
self.y = y
|
||
self.width = width
|
||
self.height = height
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
"""Сериализация в словарь"""
|
||
pass
|
||
|
||
@classmethod
|
||
def from_dict(cls, data: Dict[str, Any]) -> 'Component':
|
||
"""Десериализация из словаря"""
|
||
pass
|
||
|
||
|
||
class BasicComponent(Component):
|
||
"""Базовый компонент сайта"""
|
||
|
||
def __init__(self, component_id: str, comp_type: str, x: int, y: int,
|
||
width: int, height: int, **kwargs):
|
||
super().__init__(component_id, comp_type, x, y, width, height)
|
||
self.properties = kwargs
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
return {
|
||
'id': self.id,
|
||
'type': self.type,
|
||
'x': self.x,
|
||
'y': self.y,
|
||
'width': self.width,
|
||
'height': self.height,
|
||
'properties': self.properties
|
||
}
|
||
|
||
@classmethod
|
||
def from_dict(cls, data: Dict[str, Any]) -> 'BasicComponent':
|
||
return cls(
|
||
data['id'],
|
||
data['type'],
|
||
data['x'],
|
||
data['y'],
|
||
data['width'],
|
||
data['height'],
|
||
**data.get('properties', {})
|
||
)
|
||
|
||
|
||
class SolidityComponent(Component):
|
||
"""Компонент для работы с Solidity"""
|
||
|
||
DEFAULT_CONTRACT_CODE = """// SPDX-License-Identifier: MIT
|
||
pragma solidity ^0.8.0;
|
||
|
||
contract SimpleStorage {
|
||
uint256 private storedData;
|
||
|
||
constructor(uint256 initialValue) {
|
||
storedData = initialValue;
|
||
}
|
||
|
||
function set(uint256 x) public {
|
||
storedData = x;
|
||
}
|
||
|
||
function get() public view returns (uint256) {
|
||
return storedData;
|
||
}
|
||
|
||
function increment() public {
|
||
storedData += 1;
|
||
}
|
||
|
||
function decrement() public {
|
||
require(storedData > 0, "Value cannot be negative");
|
||
storedData -= 1;
|
||
}
|
||
}"""
|
||
|
||
def __init__(self, component_id: str, name: str, x: int, y: int, width: int, height: int):
|
||
super().__init__(component_id, "solidity", x, y, width, height)
|
||
self.name = name
|
||
self.contract_name = "SimpleStorage"
|
||
self.source_code = self.DEFAULT_CONTRACT_CODE
|
||
self.compiled = False
|
||
self.deployed = False
|
||
self.contract_address = ""
|
||
self.abi = ""
|
||
self.bytecode = ""
|
||
self.last_result = ""
|
||
self.network_url = "http://localhost:8545"
|
||
self.account = ""
|
||
self.chain_id = 1337 # Ganache по умолчанию
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
"""Сериализация"""
|
||
data = {
|
||
'id': self.id,
|
||
'name': self.name,
|
||
'type': self.type,
|
||
'x': self.x,
|
||
'y': self.y,
|
||
'width': self.width,
|
||
'height': self.height,
|
||
'contract_name': self.contract_name,
|
||
'source_code': self.source_code,
|
||
'compiled': self.compiled,
|
||
'deployed': self.deployed,
|
||
'contract_address': self.contract_address,
|
||
'network_url': self.network_url,
|
||
'account': self.account,
|
||
'chain_id': self.chain_id
|
||
}
|
||
return data
|
||
|
||
@classmethod
|
||
def from_dict(cls, data: Dict[str, Any]) -> 'SolidityComponent':
|
||
component = cls(
|
||
data['id'],
|
||
data.get('name', 'Контракт Solidity'),
|
||
data['x'],
|
||
data['y'],
|
||
data['width'],
|
||
data['height']
|
||
)
|
||
component.contract_name = data.get('contract_name', 'SimpleStorage')
|
||
component.source_code = data.get('source_code', component.DEFAULT_CONTRACT_CODE)
|
||
component.compiled = data.get('compiled', False)
|
||
component.deployed = data.get('deployed', False)
|
||
component.contract_address = data.get('contract_address', '')
|
||
component.network_url = data.get('network_url', 'http://localhost:8545')
|
||
component.account = data.get('account', '')
|
||
component.chain_id = data.get('chain_id', 1337)
|
||
return component
|
||
|
||
|
||
# ================ СЕРВИСНЫЕ КЛАССЫ ================
|
||
|
||
class Web3Deployer:
|
||
"""Сервис для деплоя контрактов"""
|
||
|
||
def __init__(self, network_url: str, chain_id: int = 1337):
|
||
self.network_url = network_url
|
||
self.chain_id = chain_id
|
||
self.w3 = None
|
||
self._connect()
|
||
|
||
def _connect(self) -> bool:
|
||
"""Подключение к сети"""
|
||
try:
|
||
self.w3 = Web3(Web3.HTTPProvider(self.network_url))
|
||
|
||
# Добавляем PoA middleware для сетей типа Ganache
|
||
if "localhost" in self.network_url or "127.0.0.1" in self.network_url:
|
||
self.w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0) # Исправлено
|
||
|
||
return self.w3.is_connected()
|
||
except Exception as e:
|
||
print(f"Ошибка подключения: {e}")
|
||
return False
|
||
|
||
def is_connected(self) -> bool:
|
||
"""Проверка подключения"""
|
||
return self.w3 and self.w3.is_connected()
|
||
|
||
def get_account_balance(self, address: str) -> Optional[int]:
|
||
"""Получение баланса аккаунта"""
|
||
try:
|
||
return self.w3.eth.get_balance(address)
|
||
except:
|
||
return None
|
||
|
||
def deploy_contract(self, abi: list, bytecode: str,
|
||
private_key: str, constructor_args: tuple = (),
|
||
gas_limit: int = 3000000) -> Dict[str, Any]:
|
||
"""Реальный деплой контракта"""
|
||
if not self.is_connected():
|
||
raise ConnectionError("Нет подключения к сети")
|
||
|
||
try:
|
||
# Создаем аккаунт из приватного ключа
|
||
account: LocalAccount = Account.from_key(private_key)
|
||
|
||
# Получаем nonce
|
||
nonce = self.w3.eth.get_transaction_count(account.address)
|
||
|
||
# Получаем актуальную цену газа
|
||
gas_price = self.w3.eth.gas_price
|
||
|
||
# Создаем объект контракта
|
||
contract = self.w3.eth.contract(abi=abi, bytecode=bytecode)
|
||
|
||
# Строим транзакцию для конструктора
|
||
transaction = contract.constructor(*constructor_args).build_transaction({
|
||
'chainId': self.chain_id,
|
||
'gas': gas_limit,
|
||
'gasPrice': gas_price,
|
||
'nonce': nonce,
|
||
'from': account.address
|
||
})
|
||
|
||
# Подписываем транзакцию
|
||
signed_txn = self.w3.eth.account.sign_transaction(transaction, private_key)
|
||
|
||
# Отправляем транзакцию
|
||
tx_hash = self.w3.eth.send_raw_transaction(signed_txn.rawTransaction)
|
||
|
||
# Ждем подтверждения
|
||
tx_receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
|
||
|
||
return {
|
||
'success': True,
|
||
'contract_address': tx_receipt.contractAddress,
|
||
'transaction_hash': tx_hash.hex(),
|
||
'block_number': tx_receipt.blockNumber,
|
||
'gas_used': tx_receipt.gasUsed,
|
||
'account': account.address
|
||
}
|
||
|
||
except TimeExhausted:
|
||
raise TimeoutError("Транзакция не подтвердилась в течение 2 минут")
|
||
except ContractLogicError as e:
|
||
raise ValueError(f"Ошибка логики контракта: {str(e)}")
|
||
except Exception as e:
|
||
raise Exception(f"Ошибка деплоя: {str(e)}")
|
||
|
||
def test_contract_function(self, contract_address: str, abi: list,
|
||
function_name: str, args: tuple = ()) -> Any:
|
||
"""Тестирование функции контракта"""
|
||
try:
|
||
contract = self.w3.eth.contract(address=contract_address, abi=abi)
|
||
|
||
# Определяем тип функции (view/pure или transactional)
|
||
function = getattr(contract.functions, function_name)
|
||
|
||
# Для view/pure функций
|
||
if function_name in ['get', 'balanceOf', 'totalSupply']:
|
||
result = function(*args).call()
|
||
return {'success': True, 'result': result, 'type': 'call'}
|
||
|
||
# Для остальных функций (нужен аккаунт для теста)
|
||
return {'success': True, 'type': 'transaction', 'message': f'Функция {function_name} готова к вызову'}
|
||
|
||
except Exception as e:
|
||
return {'success': False, 'error': str(e)}
|
||
|
||
|
||
class PrivateKeyDialog:
|
||
"""Диалог для безопасного ввода приватного ключа"""
|
||
|
||
def __init__(self, parent, title: str = "Введите приватный ключ"):
|
||
self.parent = parent
|
||
self.title = title
|
||
self.result = None
|
||
|
||
def show(self) -> Optional[str]:
|
||
"""Показать диалог и вернуть приватный ключ"""
|
||
dialog = tk.Toplevel(self.parent)
|
||
dialog.title(self.title)
|
||
dialog.geometry("500x200")
|
||
dialog.resizable(False, False)
|
||
dialog.transient(self.parent)
|
||
dialog.grab_set()
|
||
|
||
# Центрируем диалог
|
||
dialog.update_idletasks()
|
||
x = self.parent.winfo_x() + (self.parent.winfo_width() - dialog.winfo_width()) // 2
|
||
y = self.parent.winfo_y() + (self.parent.winfo_height() - dialog.winfo_height()) // 2
|
||
dialog.geometry(f"+{x}+{y}")
|
||
|
||
# Заголовок
|
||
tk.Label(dialog, text="Введите приватный ключ (начинается с 0x):",
|
||
font=('Arial', 10)).pack(pady=(20, 10))
|
||
|
||
# Поле ввода с маскировкой
|
||
key_var = tk.StringVar()
|
||
entry = tk.Entry(dialog, textvariable=key_var, width=70, show="*",
|
||
font=('Consolas', 10))
|
||
entry.pack(pady=10, padx=20)
|
||
|
||
# Кнопка показать/скрыть ключ
|
||
show_var = tk.BooleanVar(value=False)
|
||
|
||
def toggle_show():
|
||
if show_var.get():
|
||
entry.config(show="")
|
||
show_btn.config(text="👁️ Скрыть")
|
||
else:
|
||
entry.config(show="*")
|
||
show_btn.config(text="👁️ Показать")
|
||
|
||
show_btn = tk.Button(dialog, text="👁️ Показать", command=toggle_show)
|
||
show_btn.pack(pady=5)
|
||
|
||
# Фрейм для кнопок
|
||
btn_frame = tk.Frame(dialog)
|
||
btn_frame.pack(pady=20)
|
||
|
||
def on_ok():
|
||
key = key_var.get().strip()
|
||
if self._validate_private_key(key):
|
||
self.result = key
|
||
dialog.destroy()
|
||
else:
|
||
messagebox.showerror("Ошибка",
|
||
"Неверный формат приватного ключа!\n"
|
||
"Ключ должен начинаться с 0x и иметь длину 64 символа (без 0x).\n"
|
||
"Пример: 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")
|
||
|
||
def on_cancel():
|
||
self.result = None
|
||
dialog.destroy()
|
||
|
||
def on_generate():
|
||
"""Генерация нового приватного ключа"""
|
||
private_key = "0x" + secrets.token_hex(32)
|
||
key_var.set(private_key)
|
||
messagebox.showinfo("Сгенерирован ключ",
|
||
f"Сгенерирован новый приватный ключ.\n"
|
||
f"Сохраните его в безопасном месте!\n\n"
|
||
f"Адрес кошелька: {Account.from_key(private_key).address}")
|
||
|
||
tk.Button(btn_frame, text="Сгенерировать", bg="#FF9800", fg="white",
|
||
command=on_generate, width=12).pack(side=tk.LEFT, padx=5)
|
||
|
||
tk.Button(btn_frame, text="OK", bg="#4CAF50", fg="white",
|
||
command=on_ok, width=10).pack(side=tk.LEFT, padx=5)
|
||
|
||
tk.Button(btn_frame, text="Отмена", bg="#f44336", fg="white",
|
||
command=on_cancel, width=10).pack(side=tk.LEFT, padx=5)
|
||
|
||
# Бинд на Enter
|
||
dialog.bind('<Return>', lambda e: on_ok())
|
||
entry.focus_set()
|
||
|
||
dialog.wait_window()
|
||
return self.result
|
||
|
||
def _validate_private_key(self, key: str) -> bool:
|
||
"""Валидация приватного ключа"""
|
||
if not key or not key.startswith('0x'):
|
||
return False
|
||
|
||
# Проверяем длину (0x + 64 hex символа)
|
||
if len(key) != 66:
|
||
return False
|
||
|
||
# Проверяем hex формат
|
||
try:
|
||
int(key, 16)
|
||
return True
|
||
except:
|
||
return False
|
||
|
||
|
||
# ================ ГЛАВНОЕ ОКНО ================
|
||
|
||
class SolidityBuilder:
|
||
def __init__(self, root: tk.Tk):
|
||
self.root = root
|
||
self.root.title("Solidity Web3 Builder")
|
||
self.root.geometry("1300x800")
|
||
self.root.minsize(800, 600)
|
||
|
||
# Инициализация solc
|
||
self.setup_solc()
|
||
|
||
# Проект
|
||
self.components = []
|
||
self.selected_component = None
|
||
self.current_file = None
|
||
|
||
# Цвета
|
||
self.bg_color = "#1a1a2e"
|
||
self.sidebar_color = "#16213e"
|
||
self.text_color = "#ffffff"
|
||
self.accent_color = "#4a69bd"
|
||
|
||
# Инициализация атрибутов для свойств
|
||
self.prop_vars = {}
|
||
self.prop_widgets = {}
|
||
|
||
self.setup_ui()
|
||
self.add_default_components()
|
||
|
||
# Бинд для закрытия окна
|
||
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
|
||
|
||
def setup_solc(self):
|
||
"""Настраиваем Solidity компилятор"""
|
||
try:
|
||
# Устанавливаем solc если не установлен
|
||
installed = get_installed_solc_versions()
|
||
if not installed:
|
||
install_solc('0.8.0')
|
||
else:
|
||
set_solc_version(installed[0])
|
||
except Exception as e:
|
||
print(f"Ошибка настройки solc: {e}")
|
||
|
||
def on_closing(self):
|
||
"""Обработка закрытия окна"""
|
||
if self.components and not self.current_file:
|
||
response = messagebox.askyesnocancel(
|
||
"Выход",
|
||
"Сохранить проект перед выходом?"
|
||
)
|
||
|
||
if response is None: # Отмена
|
||
return
|
||
elif response: # Да
|
||
self.save_project()
|
||
|
||
self.root.destroy()
|
||
|
||
def setup_ui(self):
|
||
"""Настройка интерфейса"""
|
||
# Главный контейнер
|
||
main_container = tk.Frame(self.root, bg=self.bg_color)
|
||
main_container.pack(fill=tk.BOTH, expand=True)
|
||
|
||
# Левая панель - Компоненты
|
||
self.create_component_panel(main_container)
|
||
|
||
# Центральная область - Холст
|
||
self.create_canvas_area(main_container)
|
||
|
||
# Правая панель - Свойства
|
||
self.create_properties_panel(main_container)
|
||
|
||
# Меню
|
||
self.create_menu()
|
||
|
||
# Статус бар
|
||
self.create_status_bar()
|
||
|
||
def create_status_bar(self):
|
||
"""Создаем статус бар"""
|
||
self.status_bar = tk.Label(self.root, text="Готово", bd=1, relief=tk.SUNKEN, anchor=tk.W)
|
||
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
|
||
|
||
def update_status(self, message: str):
|
||
"""Обновление статус бара"""
|
||
self.status_bar.config(text=message)
|
||
self.root.update_idletasks()
|
||
|
||
def create_menu(self):
|
||
"""Создаем меню"""
|
||
menubar = tk.Menu(self.root)
|
||
self.root.config(menu=menubar)
|
||
|
||
# Файл
|
||
file_menu = tk.Menu(menubar, tearoff=0)
|
||
menubar.add_cascade(label="Файл", menu=file_menu)
|
||
file_menu.add_command(label="Новый проект", command=self.new_project)
|
||
file_menu.add_command(label="Открыть", command=self.open_project)
|
||
file_menu.add_command(label="Сохранить", command=self.save_project)
|
||
file_menu.add_command(label="Сохранить как", command=self.save_project_as)
|
||
file_menu.add_separator()
|
||
file_menu.add_command(label="Экспорт сайта", command=self.export_site)
|
||
file_menu.add_separator()
|
||
file_menu.add_command(label="Выход", command=self.root.quit)
|
||
|
||
# Инструменты
|
||
tools_menu = tk.Menu(menubar, tearoff=0)
|
||
menubar.add_cascade(label="Инструменты", menu=tools_menu)
|
||
tools_menu.add_command(label="Компилятор Solidity", command=self.open_solidity_compiler)
|
||
tools_menu.add_command(label="Тест сети", command=self.test_network)
|
||
tools_menu.add_command(label="Генератор контрактов", command=self.generate_contract)
|
||
tools_menu.add_separator()
|
||
tools_menu.add_command(label="Генератор кошелька", command=self.generate_wallet)
|
||
|
||
# Помощь
|
||
help_menu = tk.Menu(menubar, tearoff=0)
|
||
menubar.add_cascade(label="Помощь", menu=help_menu)
|
||
help_menu.add_command(label="Документация", command=self.show_docs)
|
||
help_menu.add_command(label="Примеры контрактов", command=self.show_examples)
|
||
help_menu.add_separator()
|
||
help_menu.add_command(label="О программе", command=self.show_about)
|
||
|
||
def create_component_panel(self, parent):
|
||
"""Панель компонентов"""
|
||
sidebar = tk.Frame(parent, width=200, bg=self.sidebar_color)
|
||
sidebar.pack(side=tk.LEFT, fill=tk.Y)
|
||
sidebar.pack_propagate(False)
|
||
|
||
tk.Label(sidebar, text="Компоненты", bg=self.sidebar_color,
|
||
fg=self.text_color, font=('Arial', 12, 'bold')).pack(pady=20)
|
||
|
||
# Базовые компоненты
|
||
tk.Label(sidebar, text="Базовые:", bg=self.sidebar_color,
|
||
fg=self.text_color, font=('Arial', 10)).pack(anchor='w', padx=10, pady=(10, 0))
|
||
|
||
basic_components = [
|
||
("Заголовок", "header"),
|
||
("Текст", "paragraph"),
|
||
("Кнопка", "button"),
|
||
("Изображение", "image")
|
||
]
|
||
|
||
for name, ctype in basic_components:
|
||
btn = tk.Button(sidebar, text=name, bg=self.accent_color, fg=self.text_color,
|
||
relief=tk.FLAT, font=('Arial', 10),
|
||
command=lambda t=ctype: self.add_basic_component(t))
|
||
btn.pack(pady=5, padx=10, fill=tk.X)
|
||
|
||
# Web3 компоненты
|
||
tk.Label(sidebar, text="Web3:", bg=self.sidebar_color,
|
||
fg=self.text_color, font=('Arial', 10)).pack(anchor='w', padx=10, pady=(20, 0))
|
||
|
||
# Только контракты Solidity
|
||
btn = tk.Button(sidebar, text="Контракт Solidity", bg=self.accent_color, fg=self.text_color,
|
||
relief=tk.FLAT, font=('Arial', 10),
|
||
command=lambda: self.add_solidity_component())
|
||
btn.pack(pady=5, padx=10, fill=tk.X)
|
||
|
||
# Кнопки управления
|
||
tk.Frame(sidebar, height=20, bg=self.sidebar_color).pack() # Отступ
|
||
|
||
export_btn = tk.Button(sidebar, text="🚀 Экспорт", bg="#4CAF50", fg="white",
|
||
font=('Arial', 11, 'bold'), command=self.export_site)
|
||
export_btn.pack(side=tk.BOTTOM, pady=20, padx=10, fill=tk.X)
|
||
|
||
preview_btn = tk.Button(sidebar, text="👁️ Предпросмотр", bg="#2196F3", fg="white",
|
||
font=('Arial', 10), command=self.preview_site)
|
||
preview_btn.pack(side=tk.BOTTOM, pady=5, padx=10, fill=tk.X)
|
||
|
||
def create_properties_panel(self, parent):
|
||
"""Панель свойств компонентов"""
|
||
# Правая панель
|
||
self.props_frame = tk.Frame(parent, width=300, bg=self.sidebar_color)
|
||
self.props_frame.pack(side=tk.RIGHT, fill=tk.Y)
|
||
self.props_frame.pack_propagate(False)
|
||
|
||
# Заголовок панели свойств
|
||
self.props_title = tk.Label(self.props_frame, text="Свойства",
|
||
bg=self.sidebar_color, fg=self.text_color,
|
||
font=('Arial', 12, 'bold'))
|
||
self.props_title.pack(pady=20)
|
||
|
||
# Контейнер для свойств
|
||
self.props_content = tk.Frame(self.props_frame, bg=self.sidebar_color)
|
||
self.props_content.pack(fill=tk.BOTH, expand=True, padx=10)
|
||
|
||
# Инициализация словарей для свойств
|
||
self.prop_vars = {}
|
||
self.prop_widgets = {}
|
||
|
||
def create_canvas_area(self, parent):
|
||
"""Область холста"""
|
||
canvas_frame = tk.Frame(parent, bg=self.bg_color)
|
||
canvas_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||
|
||
# Панель инструментов холста
|
||
toolbar = tk.Frame(canvas_frame, bg=self.sidebar_color)
|
||
toolbar.pack(fill=tk.X, pady=(0, 10))
|
||
|
||
tk.Label(toolbar, text="Холст", bg=self.sidebar_color,
|
||
fg=self.text_color, font=('Arial', 11, 'bold')).pack(side=tk.LEFT, padx=10)
|
||
|
||
# Холст
|
||
self.canvas = tk.Canvas(canvas_frame, bg="#0f3460", highlightthickness=1,
|
||
highlightbackground="#4a69bd", scrollregion=(0, 0, 2000, 2000))
|
||
|
||
# Скроллбары
|
||
v_scrollbar = ttk.Scrollbar(canvas_frame, orient=tk.VERTICAL, command=self.canvas.yview)
|
||
h_scrollbar = ttk.Scrollbar(canvas_frame, orient=tk.HORIZONTAL, command=self.canvas.xview)
|
||
self.canvas.configure(xscrollcommand=h_scrollbar.set, yscrollcommand=v_scrollbar.set)
|
||
|
||
# Размещение
|
||
h_scrollbar.pack(side=tk.BOTTOM, fill=tk.X)
|
||
v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||
|
||
# Сетка
|
||
self.draw_grid()
|
||
|
||
# Бинды для перетаскивания
|
||
self.canvas.bind("<Button-1>", self.on_canvas_click)
|
||
self.canvas.bind("<B1-Motion>", self.on_canvas_drag)
|
||
self.canvas.bind("<ButtonRelease-1>", self.on_canvas_release)
|
||
self.canvas.bind("<Button-3>", self.on_canvas_right_click)
|
||
|
||
self.drag_data = {"x": 0, "y": 0, "item": None}
|
||
|
||
def draw_grid(self):
|
||
"""Рисуем сетку на холсте"""
|
||
self.canvas.delete("grid")
|
||
|
||
width = 2000
|
||
height = 2000
|
||
grid_size = 20
|
||
|
||
# Вертикальные линии
|
||
for x in range(0, width, grid_size):
|
||
self.canvas.create_line(x, 0, x, height, fill="#2a3b5c", tags="grid", width=1)
|
||
|
||
# Горизонтальные линии
|
||
for y in range(0, height, grid_size):
|
||
self.canvas.create_line(0, y, width, y, fill="#2a3b5c", tags="grid", width=1)
|
||
|
||
def add_default_components(self):
|
||
"""Добавляем компоненты по умолчанию"""
|
||
# Заголовок
|
||
header = BasicComponent(
|
||
"header_1", "header", 100, 50, 800, 80,
|
||
text="Solidity Web3 Сайт",
|
||
color="#ffffff",
|
||
bg_color="#0f3460",
|
||
font_size=36
|
||
)
|
||
self.components.append(header)
|
||
self.draw_basic_component(header)
|
||
|
||
# Текст
|
||
paragraph = BasicComponent(
|
||
"para_1", "paragraph", 100, 150, 600, 100,
|
||
text="Создайте свой сайт с поддержкой смарт-контрактов Solidity\nИспользуйте Ganache для локального тестирования",
|
||
color="#cccccc",
|
||
font_size=16
|
||
)
|
||
self.components.append(paragraph)
|
||
self.draw_basic_component(paragraph)
|
||
|
||
def add_basic_component(self, comp_type):
|
||
"""Добавляем базовый компонент"""
|
||
comp_id = f"{comp_type}_{len(self.components) + 1}"
|
||
|
||
defaults = {
|
||
'header': {
|
||
'text': 'Новый заголовок',
|
||
'color': '#ffffff',
|
||
'bg_color': '#0f3460',
|
||
'font_size': 24,
|
||
'width': 400, 'height': 60
|
||
},
|
||
'paragraph': {
|
||
'text': 'Введите текст здесь...',
|
||
'color': '#cccccc',
|
||
'font_size': 14,
|
||
'width': 500, 'height': 80
|
||
},
|
||
'button': {
|
||
'text': 'Кнопка',
|
||
'color': '#ffffff',
|
||
'bg_color': '#4CAF50',
|
||
'font_size': 14,
|
||
'width': 120, 'height': 40
|
||
},
|
||
'image': {
|
||
'src': 'https://placehold.co/400x300',
|
||
'alt': 'Изображение',
|
||
'width': 400, 'height': 300
|
||
}
|
||
}
|
||
|
||
props = defaults.get(comp_type, defaults['header']).copy()
|
||
width = props.pop('width')
|
||
height = props.pop('height')
|
||
|
||
component = BasicComponent(
|
||
comp_id, comp_type,
|
||
100, 100 + len(self.components) * 50,
|
||
width, height,
|
||
**props
|
||
)
|
||
|
||
self.components.append(component)
|
||
self.draw_basic_component(component)
|
||
self.select_component(component)
|
||
|
||
def add_solidity_component(self):
|
||
"""Добавляем компонент Solidity"""
|
||
comp_id = f"solidity_{len(self.components) + 1}"
|
||
component = SolidityComponent(
|
||
comp_id, "Контракт Solidity",
|
||
100, 100 + len(self.components) * 50,
|
||
600, 400
|
||
)
|
||
self.components.append(component)
|
||
self.draw_solidity_component(component)
|
||
self.select_component(component)
|
||
|
||
def draw_basic_component(self, component):
|
||
"""Рисуем базовый компонент на холсте"""
|
||
x, y = component.x, component.y
|
||
width, height = component.width, component.height
|
||
|
||
# Рисуем прямоугольник
|
||
rect = self.canvas.create_rectangle(
|
||
x, y, x + width, y + height,
|
||
fill=component.properties.get('bg_color', '#2d4059'),
|
||
outline='#4a69bd',
|
||
width=2,
|
||
tags=('component', component.id)
|
||
)
|
||
|
||
# Добавляем текст в зависимости от типа
|
||
if component.type == 'header':
|
||
self.canvas.create_text(
|
||
x + width/2, y + height/2,
|
||
text=component.properties.get('text', ''),
|
||
fill=component.properties.get('color', '#ffffff'),
|
||
font=('Arial', component.properties.get('font_size', 24)),
|
||
tags=('text', component.id)
|
||
)
|
||
elif component.type == 'paragraph':
|
||
self.canvas.create_text(
|
||
x + width/2, y + height/2,
|
||
text=component.properties.get('text', ''),
|
||
fill=component.properties.get('color', '#cccccc'),
|
||
font=('Arial', component.properties.get('font_size', 14)),
|
||
width=width - 20,
|
||
tags=('text', component.id)
|
||
)
|
||
elif component.type == 'button':
|
||
self.canvas.create_text(
|
||
x + width/2, y + height/2,
|
||
text=component.properties.get('text', 'Кнопка'),
|
||
fill=component.properties.get('color', '#ffffff'),
|
||
font=('Arial', component.properties.get('font_size', 14)),
|
||
tags=('text', component.id)
|
||
)
|
||
elif component.type == 'image':
|
||
self.canvas.create_text(
|
||
x + width/2, y + height/2,
|
||
text='🖼️ Изображение',
|
||
fill='#ffffff',
|
||
font=('Arial', 14),
|
||
tags=('text', component.id)
|
||
)
|
||
|
||
# Метка с типом
|
||
label_y = y + height + 15
|
||
self.canvas.create_text(
|
||
x + width/2, label_y,
|
||
text=component.type.title(),
|
||
fill='#cccccc',
|
||
font=('Arial', 9),
|
||
tags=('label', component.id)
|
||
)
|
||
|
||
def draw_solidity_component(self, component):
|
||
"""Рисуем компонент Solidity на холсте"""
|
||
x, y = component.x, component.y
|
||
width, height = component.width, component.height
|
||
|
||
# Рисуем специальный прямоугольник для контракта
|
||
rect = self.canvas.create_rectangle(
|
||
x, y, x + width, y + height,
|
||
fill='#1c2541',
|
||
outline='#5bc0be',
|
||
width=3,
|
||
tags=('component', 'solidity', component.id)
|
||
)
|
||
|
||
# Иконка Solidity
|
||
self.canvas.create_text(
|
||
x + 30, y + 30,
|
||
text="🛠️",
|
||
font=('Arial', 24),
|
||
tags=('icon', component.id)
|
||
)
|
||
|
||
# Название контракта
|
||
self.canvas.create_text(
|
||
x + width/2, y + 40,
|
||
text=f"Контракт: {component.contract_name}",
|
||
fill='#5bc0be',
|
||
font=('Arial', 16, 'bold'),
|
||
tags=('title', component.id)
|
||
)
|
||
|
||
# Статус
|
||
status = "Не скомпилирован"
|
||
status_color = "#ff6b6b"
|
||
|
||
if component.compiled:
|
||
status = "✓ Скомпилирован"
|
||
status_color = "#4ecdc4"
|
||
|
||
if component.deployed:
|
||
status = f"✓ Деплоен"
|
||
status_color = "#4CAF50"
|
||
|
||
self.canvas.create_text(
|
||
x + width/2, y + 80,
|
||
text=status,
|
||
fill=status_color,
|
||
font=('Arial', 12),
|
||
tags=('status', component.id)
|
||
)
|
||
|
||
# Адрес контракта (если деплоен)
|
||
if component.deployed and component.contract_address:
|
||
self.canvas.create_text(
|
||
x + width/2, y + 110,
|
||
text=f"Адрес: {component.contract_address[:12]}...{component.contract_address[-10:]}",
|
||
fill='#aaaaaa',
|
||
font=('Arial', 9),
|
||
tags=('address', component.id)
|
||
)
|
||
|
||
# Метка
|
||
label_y = y + height + 15
|
||
self.canvas.create_text(
|
||
x + width/2, label_y,
|
||
text="Solidity Контракт",
|
||
fill='#5bc0be',
|
||
font=('Arial', 10, 'bold'),
|
||
tags=('label', component.id)
|
||
)
|
||
|
||
def on_canvas_click(self, event):
|
||
"""Обработка клика на холсте"""
|
||
x, y = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y)
|
||
items = self.canvas.find_overlapping(x-5, y-5, x+5, y+5)
|
||
|
||
for item in items:
|
||
tags = self.canvas.gettags(item)
|
||
for tag in tags:
|
||
if tag in [comp.id for comp in self.components]:
|
||
self.select_component_by_id(tag)
|
||
self.drag_data["item"] = tag
|
||
self.drag_data["x"] = x
|
||
self.drag_data["y"] = y
|
||
return
|
||
|
||
self.clear_selection()
|
||
|
||
def on_canvas_drag(self, event):
|
||
"""Обработка перетаскивания"""
|
||
if self.drag_data["item"]:
|
||
x = self.canvas.canvasx(event.x)
|
||
y = self.canvas.canvasy(event.y)
|
||
dx = x - self.drag_data["x"]
|
||
dy = y - self.drag_data["y"]
|
||
|
||
if dx != 0 or dy != 0:
|
||
items = self.canvas.find_withtag(self.drag_data["item"])
|
||
for item in items:
|
||
self.canvas.move(item, dx, dy)
|
||
|
||
self.drag_data["x"] = x
|
||
self.drag_data["y"] = y
|
||
|
||
def on_canvas_release(self, event):
|
||
"""Обработка отпускания кнопки мыши"""
|
||
if self.drag_data["item"]:
|
||
component = self.get_component_by_id(self.drag_data["item"])
|
||
if component:
|
||
# Обновляем позицию компонента
|
||
items = self.canvas.find_withtag(component.id)
|
||
for item in items:
|
||
if self.canvas.type(item) == 'rectangle':
|
||
coords = self.canvas.coords(item)
|
||
component.x = coords[0]
|
||
component.y = coords[1]
|
||
break
|
||
|
||
self.drag_data["item"] = None
|
||
|
||
def on_canvas_right_click(self, event):
|
||
"""Правый клик - контекстное меню"""
|
||
x, y = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y)
|
||
items = self.canvas.find_overlapping(x-5, y-5, x+5, y+5)
|
||
|
||
for item in items:
|
||
tags = self.canvas.gettags(item)
|
||
for tag in tags:
|
||
if tag in [comp.id for comp in self.components]:
|
||
self.select_component_by_id(tag)
|
||
|
||
menu = tk.Menu(self.root, tearoff=0)
|
||
menu.add_command(label="Удалить", command=self.delete_selected)
|
||
menu.add_separator()
|
||
menu.add_command(label="Дублировать", command=self.duplicate_selected)
|
||
menu.add_command(label="Переименовать", command=self.rename_selected)
|
||
|
||
if self.selected_component and self.selected_component.type == "solidity":
|
||
menu.add_separator()
|
||
menu.add_command(label="Скомпилировать", command=self.compile_selected_contract)
|
||
menu.add_command(label="Деплоить", command=self.deploy_selected_contract)
|
||
menu.add_command(label="Тестировать", command=lambda: self.test_contract_function("get"))
|
||
|
||
try:
|
||
menu.tk_popup(event.x_root, event.y_root)
|
||
finally:
|
||
menu.grab_release()
|
||
return
|
||
|
||
self.clear_selection()
|
||
|
||
def select_component(self, component):
|
||
"""Выделяем компонент"""
|
||
# Сначала снимаем выделение со всех компонентов
|
||
for comp in self.components:
|
||
items = self.canvas.find_withtag(comp.id)
|
||
for item in items:
|
||
if self.canvas.type(item) == 'rectangle':
|
||
if comp.type == "solidity":
|
||
self.canvas.itemconfig(item, outline='#5bc0be', width=3)
|
||
else:
|
||
self.canvas.itemconfig(item, outline='#4a69bd', width=2)
|
||
|
||
# Выделяем выбранный компонент
|
||
items = self.canvas.find_withtag(component.id)
|
||
for item in items:
|
||
if self.canvas.type(item) == 'rectangle':
|
||
self.canvas.itemconfig(item, outline='#FFD700', width=4) # Золотая рамка для выделения
|
||
|
||
self.selected_component = component
|
||
self.update_properties_panel()
|
||
|
||
def clear_selection(self):
|
||
"""Снимаем выделение"""
|
||
if self.selected_component:
|
||
component = self.selected_component
|
||
items = self.canvas.find_withtag(component.id)
|
||
for item in items:
|
||
if self.canvas.type(item) == 'rectangle':
|
||
if component.type == "solidity":
|
||
self.canvas.itemconfig(item, outline='#5bc0be', width=3)
|
||
else:
|
||
self.canvas.itemconfig(item, outline='#4a69bd', width=2)
|
||
|
||
self.selected_component = None
|
||
self.update_properties_panel()
|
||
|
||
def select_component_by_id(self, component_id):
|
||
"""Выделяем компонент по ID"""
|
||
component = self.get_component_by_id(component_id)
|
||
if component:
|
||
self.select_component(component)
|
||
|
||
def get_component_by_id(self, component_id):
|
||
"""Получаем компонент по ID"""
|
||
for comp in self.components:
|
||
if comp.id == component_id:
|
||
return comp
|
||
return None
|
||
|
||
def delete_selected(self):
|
||
"""Удаляем выбранный компонент"""
|
||
if not self.selected_component:
|
||
return
|
||
|
||
items = self.canvas.find_withtag(self.selected_component.id)
|
||
for item in items:
|
||
self.canvas.delete(item)
|
||
|
||
self.components.remove(self.selected_component)
|
||
self.selected_component = None
|
||
self.update_properties_panel()
|
||
|
||
def duplicate_selected(self):
|
||
"""Дублируем выбранный компонент"""
|
||
if not self.selected_component:
|
||
return
|
||
|
||
comp = self.selected_component
|
||
|
||
if comp.type == "solidity":
|
||
new_comp = SolidityComponent(
|
||
f"solidity_{len(self.components) + 1}",
|
||
f"{comp.name} (копия)",
|
||
comp.x + 50,
|
||
comp.y + 50,
|
||
comp.width,
|
||
comp.height
|
||
)
|
||
new_comp.contract_name = comp.contract_name
|
||
new_comp.source_code = comp.source_code
|
||
new_comp.network_url = comp.network_url
|
||
new_comp.account = comp.account
|
||
new_comp.chain_id = comp.chain_id
|
||
new_comp.compiled = comp.compiled
|
||
new_comp.deployed = comp.deployed
|
||
new_comp.contract_address = comp.contract_address
|
||
new_comp.abi = comp.abi
|
||
new_comp.bytecode = comp.bytecode
|
||
else:
|
||
new_comp = BasicComponent(
|
||
f"{comp.type}_{len(self.components) + 1}",
|
||
comp.type,
|
||
comp.x + 50,
|
||
comp.y + 50,
|
||
comp.width,
|
||
comp.height,
|
||
**comp.properties
|
||
)
|
||
|
||
self.components.append(new_comp)
|
||
|
||
if comp.type == "solidity":
|
||
self.draw_solidity_component(new_comp)
|
||
else:
|
||
self.draw_basic_component(new_comp)
|
||
|
||
self.select_component(new_comp)
|
||
|
||
def rename_selected(self):
|
||
"""Переименовываем компонент"""
|
||
if not self.selected_component:
|
||
return
|
||
|
||
if self.selected_component.type == "solidity":
|
||
new_name = tk.simpledialog.askstring(
|
||
"Переименовать контракт",
|
||
"Введите новое имя контракта:",
|
||
initialvalue=self.selected_component.contract_name
|
||
)
|
||
if new_name:
|
||
self.selected_component.contract_name = new_name
|
||
self.redraw_component(self.selected_component)
|
||
self.update_properties_panel()
|
||
|
||
def update_properties_panel(self):
|
||
"""Обновляем панель свойств"""
|
||
# Очищаем панель
|
||
for widget in self.props_content.winfo_children():
|
||
widget.destroy()
|
||
|
||
self.prop_vars.clear()
|
||
self.prop_widgets.clear()
|
||
|
||
if not self.selected_component:
|
||
self.props_title.config(text="Свойства")
|
||
tk.Label(self.props_content, text="Выберите компонент",
|
||
bg=self.sidebar_color, fg='#aaaaaa').pack(pady=50)
|
||
return
|
||
|
||
# Настраиваем заголовок
|
||
comp_type = self.selected_component.type
|
||
if comp_type == "solidity":
|
||
self.props_title.config(text=f"Контракт: {self.selected_component.contract_name}")
|
||
else:
|
||
self.props_title.config(text=f"Свойства: {comp_type}")
|
||
|
||
# Создаем свойства в зависимости от типа компонента
|
||
if comp_type == "solidity":
|
||
self.create_solidity_properties()
|
||
else:
|
||
self.create_basic_properties()
|
||
|
||
def create_basic_properties(self):
|
||
"""Создаем свойства для базового компонента"""
|
||
row = 0
|
||
|
||
# Позиция и размер
|
||
self.create_property_field("Позиция X", "x", self.selected_component.x, row)
|
||
row += 1
|
||
|
||
self.create_property_field("Позиция Y", "y", self.selected_component.y, row)
|
||
row += 1
|
||
|
||
self.create_property_field("Ширина", "width", self.selected_component.width, row)
|
||
row += 1
|
||
|
||
self.create_property_field("Высота", "height", self.selected_component.height, row)
|
||
row += 1
|
||
|
||
# Свойства компонента
|
||
for prop_name, prop_value in self.selected_component.properties.items():
|
||
if isinstance(prop_value, (str, int, float)):
|
||
self.create_property_field(prop_name.capitalize(), prop_name, prop_value, row)
|
||
row += 1
|
||
|
||
# Кнопка обновления
|
||
tk.Button(self.props_content, text="Применить", bg=self.accent_color, fg=self.text_color,
|
||
command=self.apply_properties).pack(pady=20)
|
||
|
||
def create_solidity_properties(self):
|
||
"""Создаем свойства для Solidity компонента"""
|
||
component = self.selected_component
|
||
|
||
# Заголовок
|
||
tk.Label(self.props_content, text="Контракт Solidity", bg=self.sidebar_color,
|
||
fg='#5bc0be', font=('Arial', 11, 'bold')).pack(anchor='w', pady=(0, 10))
|
||
|
||
# Имя контракта
|
||
self.create_property_field("Имя контракта", "contract_name",
|
||
component.contract_name, 0)
|
||
|
||
# Кнопка редактирования кода
|
||
tk.Button(self.props_content, text="✏️ Редактировать код", bg=self.accent_color,
|
||
fg=self.text_color, command=self.edit_solidity_code).pack(fill=tk.X, pady=10)
|
||
|
||
# Состояние
|
||
status_frame = tk.Frame(self.props_content, bg=self.sidebar_color)
|
||
status_frame.pack(fill=tk.X, pady=10)
|
||
|
||
status_text = "Не скомпилирован"
|
||
if component.compiled:
|
||
status_text = "✓ Скомпилирован"
|
||
if component.deployed:
|
||
status_text = f"✓ Деплоен"
|
||
|
||
tk.Label(status_frame, text="Состояние:", bg=self.sidebar_color,
|
||
fg=self.text_color).pack(side=tk.LEFT)
|
||
tk.Label(status_frame, text=status_text, bg=self.sidebar_color,
|
||
fg='#4CAF50' if component.deployed else '#4ecdc4' if component.compiled else '#ff6b6b').pack(side=tk.RIGHT)
|
||
|
||
# Кнопки управления контрактом
|
||
btn_frame = tk.Frame(self.props_content, bg=self.sidebar_color)
|
||
btn_frame.pack(fill=tk.X, pady=10)
|
||
|
||
tk.Button(btn_frame, text="🛠️ Скомпилировать", bg="#2196F3", fg="white",
|
||
command=self.compile_selected_contract).pack(side=tk.LEFT, padx=2, fill=tk.X, expand=True)
|
||
|
||
tk.Button(btn_frame, text="🚀 Деплоить", bg="#4CAF50", fg="white",
|
||
command=self.deploy_selected_contract).pack(side=tk.LEFT, padx=2, fill=tk.X, expand=True)
|
||
|
||
# Настройки сети
|
||
tk.Label(self.props_content, text="Настройки сети:", bg=self.sidebar_color,
|
||
fg=self.text_color, font=('Arial', 10, 'bold')).pack(anchor='w', pady=(20, 5))
|
||
|
||
self.create_property_field("URL сети", "network_url",
|
||
component.network_url, 5)
|
||
|
||
self.create_property_field("Chain ID", "chain_id",
|
||
component.chain_id, 6)
|
||
|
||
# Адрес контракта (только для чтения)
|
||
if component.contract_address:
|
||
addr_frame = tk.Frame(self.props_content, bg=self.sidebar_color)
|
||
addr_frame.pack(fill=tk.X, pady=5)
|
||
|
||
tk.Label(addr_frame, text="Адрес:", bg=self.sidebar_color,
|
||
fg=self.text_color, width=10).pack(side=tk.LEFT)
|
||
|
||
addr_label = tk.Label(addr_frame, text=component.contract_address,
|
||
bg=self.sidebar_color, fg='#4CAF50', font=('Consolas', 9))
|
||
addr_label.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||
|
||
# Кнопка копирования
|
||
tk.Button(addr_frame, text="📋", bg="#2196F3", fg="white", font=('Arial', 9),
|
||
command=lambda: self.copy_to_clipboard(component.contract_address)).pack(side=tk.RIGHT)
|
||
|
||
# Тестирование функций
|
||
if component.compiled and component.deployed:
|
||
tk.Label(self.props_content, text="Тестирование:", bg=self.sidebar_color,
|
||
fg=self.text_color, font=('Arial', 10, 'bold')).pack(anchor='w', pady=(20, 5))
|
||
|
||
test_frame = tk.Frame(self.props_content, bg=self.sidebar_color)
|
||
test_frame.pack(fill=tk.X, pady=5)
|
||
|
||
tk.Button(test_frame, text="get()", bg="#9C27B0", fg="white",
|
||
command=lambda: self.test_contract_function("get")).pack(side=tk.LEFT, padx=2)
|
||
|
||
tk.Button(test_frame, text="set(100)", bg="#9C27B0", fg="white",
|
||
command=lambda: self.test_contract_function("set", 100)).pack(side=tk.LEFT, padx=2)
|
||
|
||
tk.Button(test_frame, text="increment()", bg="#9C27B0", fg="white",
|
||
command=lambda: self.test_contract_function("increment")).pack(side=tk.LEFT, padx=2)
|
||
|
||
def create_property_field(self, label, prop_name, value, row):
|
||
"""Создаем поле свойства"""
|
||
frame = tk.Frame(self.props_content, bg=self.sidebar_color)
|
||
frame.pack(fill=tk.X, pady=2)
|
||
|
||
tk.Label(frame, text=label, bg=self.sidebar_color,
|
||
fg=self.text_color, width=15, anchor='w').pack(side=tk.LEFT)
|
||
|
||
var = tk.StringVar(value=str(value))
|
||
entry = tk.Entry(frame, textvariable=var, bg="#2a3b5c",
|
||
fg=self.text_color, insertbackground=self.text_color)
|
||
entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||
|
||
self.prop_vars[prop_name] = var
|
||
|
||
def apply_properties(self):
|
||
"""Применяем измененные свойства"""
|
||
if not self.selected_component:
|
||
return
|
||
|
||
component = self.selected_component
|
||
|
||
# Обновляем позицию и размер
|
||
for prop in ['x', 'y', 'width', 'height']:
|
||
if prop in self.prop_vars:
|
||
try:
|
||
value = int(float(self.prop_vars[prop].get()))
|
||
setattr(component, prop, value)
|
||
except:
|
||
pass
|
||
|
||
# Обновляем свойства компонента
|
||
if hasattr(component, 'properties'):
|
||
for prop_name, var in self.prop_vars.items():
|
||
if prop_name not in ['x', 'y', 'width', 'height']:
|
||
component.properties[prop_name] = var.get()
|
||
elif hasattr(component, 'contract_name'):
|
||
# Для Solidity компонента
|
||
for prop_name, var in self.prop_vars.items():
|
||
if hasattr(component, prop_name):
|
||
try:
|
||
if prop_name == 'chain_id':
|
||
setattr(component, prop_name, int(var.get()))
|
||
else:
|
||
setattr(component, prop_name, var.get())
|
||
except:
|
||
pass
|
||
|
||
# Перерисовываем компонент
|
||
self.redraw_component(component)
|
||
|
||
def redraw_component(self, component):
|
||
"""Перерисовываем компонент"""
|
||
items = self.canvas.find_withtag(component.id)
|
||
for item in items:
|
||
self.canvas.delete(item)
|
||
|
||
if component.type == "solidity":
|
||
self.draw_solidity_component(component)
|
||
else:
|
||
self.draw_basic_component(component)
|
||
|
||
# === Solidity функции ===
|
||
|
||
def edit_solidity_code(self):
|
||
"""Редактирование кода Solidity"""
|
||
if not self.selected_component or self.selected_component.type != "solidity":
|
||
return
|
||
|
||
component = self.selected_component
|
||
|
||
dialog = tk.Toplevel(self.root)
|
||
dialog.title(f"Редактор Solidity: {component.contract_name}")
|
||
dialog.geometry("800x600")
|
||
|
||
# Редактор кода
|
||
editor_frame = tk.Frame(dialog)
|
||
editor_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||
|
||
tk.Label(editor_frame, text="Код контракта Solidity:",
|
||
font=('Arial', 11, 'bold')).pack(anchor='w')
|
||
|
||
editor = scrolledtext.ScrolledText(editor_frame, wrap=tk.WORD, font=('Consolas', 10))
|
||
editor.pack(fill=tk.BOTH, expand=True, pady=5)
|
||
editor.insert(1.0, component.source_code)
|
||
|
||
def save_code():
|
||
component.source_code = editor.get(1.0, tk.END)
|
||
component.compiled = False # Сбрасываем статус компиляции
|
||
dialog.destroy()
|
||
self.redraw_component(component)
|
||
|
||
# Кнопки
|
||
btn_frame = tk.Frame(dialog)
|
||
btn_frame.pack(fill=tk.X, padx=10, pady=10)
|
||
|
||
tk.Button(btn_frame, text="Сохранить", bg="#4CAF50", fg="white",
|
||
command=save_code).pack(side=tk.RIGHT, padx=5)
|
||
|
||
tk.Button(btn_frame, text="Отмена", bg="#f44336", fg="white",
|
||
command=dialog.destroy).pack(side=tk.RIGHT, padx=5)
|
||
|
||
tk.Button(btn_frame, text="Примеры", bg="#2196F3", fg="white",
|
||
command=lambda: self.insert_example_code(editor)).pack(side=tk.LEFT)
|
||
|
||
def compile_selected_contract(self):
|
||
"""Компилируем выбранный контракт"""
|
||
if not self.selected_component or self.selected_component.type != "solidity":
|
||
messagebox.showwarning("Ошибка", "Выберите компонент Solidity контракта")
|
||
return
|
||
|
||
component = self.selected_component
|
||
|
||
try:
|
||
self.update_status("Компиляция контракта...")
|
||
|
||
# Компилируем контракт
|
||
compiled_sol = compile_source(
|
||
component.source_code,
|
||
output_values=['abi', 'bin'],
|
||
solc_version='0.8.0'
|
||
)
|
||
|
||
# Ищем нужный контракт
|
||
for contract_id, contract_interface in compiled_sol.items():
|
||
if component.contract_name in contract_id:
|
||
component.abi = contract_interface['abi']
|
||
component.bytecode = contract_interface['bin']
|
||
component.compiled = True
|
||
break
|
||
else:
|
||
# Если не нашли по имени, берем первый
|
||
contract_id, contract_interface = compiled_sol.popitem()
|
||
component.abi = contract_interface['abi']
|
||
component.bytecode = contract_interface['bin']
|
||
component.compiled = True
|
||
|
||
# Обновляем отображение
|
||
self.redraw_component(component)
|
||
self.update_properties_panel()
|
||
|
||
messagebox.showinfo("Успех", f"Контракт '{component.contract_name}' успешно скомпилирован!")
|
||
self.update_status("Контракт скомпилирован")
|
||
|
||
except Exception as e:
|
||
messagebox.showerror("Ошибка компиляции", f"Ошибка: {str(e)}")
|
||
self.update_status("Ошибка компиляции")
|
||
|
||
def deploy_selected_contract(self):
|
||
"""Реальный деплой выбранного контракта"""
|
||
if not self.selected_component or self.selected_component.type != "solidity":
|
||
return
|
||
|
||
component = self.selected_component
|
||
|
||
if not component.compiled:
|
||
messagebox.showwarning("Ошибка", "Сначала скомпилируйте контракт")
|
||
return
|
||
|
||
# Запрашиваем приватный ключ
|
||
key_dialog = PrivateKeyDialog(self.root, "Приватный ключ для деплоя")
|
||
private_key = key_dialog.show()
|
||
|
||
if not private_key:
|
||
return # Пользователь отменил
|
||
|
||
# Диалог для параметров конструктора
|
||
dialog = tk.Toplevel(self.root)
|
||
dialog.title("Деплой контракта")
|
||
dialog.geometry("500x400")
|
||
|
||
tk.Label(dialog, text="Параметры деплоя", font=('Arial', 12, 'bold')).pack(pady=10)
|
||
|
||
# Параметры конструктора
|
||
tk.Label(dialog, text="Начальное значение для SimpleStorage:").pack(anchor='w', padx=20, pady=(10, 0))
|
||
initial_value = tk.Entry(dialog)
|
||
initial_value.pack(fill=tk.X, padx=20, pady=5)
|
||
initial_value.insert(0, "100")
|
||
|
||
# Лимит газа
|
||
tk.Label(dialog, text="Лимит газа (gas):").pack(anchor='w', padx=20, pady=(10, 0))
|
||
gas_limit = tk.Entry(dialog)
|
||
gas_limit.pack(fill=tk.X, padx=20, pady=5)
|
||
gas_limit.insert(0, "3000000")
|
||
|
||
# Информация о сети
|
||
info_frame = tk.Frame(dialog)
|
||
info_frame.pack(fill=tk.X, padx=20, pady=10)
|
||
|
||
tk.Label(info_frame, text=f"Сеть: {component.network_url}", anchor='w').pack(fill=tk.X)
|
||
tk.Label(info_frame, text=f"Chain ID: {component.chain_id}", anchor='w').pack(fill=tk.X)
|
||
|
||
# Получаем адрес из приватного ключа
|
||
try:
|
||
account = Account.from_key(private_key)
|
||
tk.Label(info_frame, text=f"Кошелек: {account.address[:20]}...", anchor='w').pack(fill=tk.X)
|
||
except:
|
||
pass
|
||
|
||
# Статус
|
||
status_label = tk.Label(dialog, text="", fg="#666666")
|
||
status_label.pack(pady=5)
|
||
|
||
def deploy():
|
||
try:
|
||
status_label.config(text="Подключение к сети...", fg="#2196F3")
|
||
dialog.update()
|
||
|
||
# Создаем деплоер
|
||
deployer = Web3Deployer(component.network_url, component.chain_id)
|
||
|
||
if not deployer.is_connected():
|
||
status_label.config(text="❌ Не удалось подключиться к сети", fg="#f44336")
|
||
return
|
||
|
||
status_label.config(text="✓ Подключено. Отправка транзакции...", fg="#4CAF50")
|
||
dialog.update()
|
||
|
||
# Получаем параметры
|
||
constructor_args = (int(initial_value.get()),)
|
||
gas_limit_val = int(gas_limit.get())
|
||
|
||
# Деплоим контракт
|
||
result = deployer.deploy_contract(
|
||
abi=component.abi,
|
||
bytecode=component.bytecode,
|
||
private_key=private_key,
|
||
constructor_args=constructor_args,
|
||
gas_limit=gas_limit_val
|
||
)
|
||
|
||
# Обновляем компонент
|
||
component.contract_address = result['contract_address']
|
||
component.deployed = True
|
||
component.account = result['account']
|
||
|
||
status_label.config(text="✓ Контракт успешно деплоен!", fg="#4CAF50")
|
||
dialog.update()
|
||
|
||
# Показываем результат
|
||
messagebox.showinfo("Успех",
|
||
f"Контракт успешно деплоен!\n\n"
|
||
f"Адрес контракта: {result['contract_address']}\n"
|
||
f"Хэш транзакции: {result['transaction_hash']}\n"
|
||
f"Блок: {result['block_number']}\n"
|
||
f"Газ использовано: {result['gas_used']}\n"
|
||
f"Аккаунт: {result['account']}")
|
||
|
||
# Обновляем интерфейс
|
||
self.redraw_component(component)
|
||
self.update_properties_panel()
|
||
self.update_status("Контракт деплоен")
|
||
|
||
# Закрываем диалог через 2 секунды
|
||
dialog.after(2000, dialog.destroy)
|
||
|
||
except TimeoutError as e:
|
||
status_label.config(text=f"❌ {str(e)}", fg="#f44336")
|
||
except ValueError as e:
|
||
status_label.config(text=f"❌ {str(e)}", fg="#f44336")
|
||
except Exception as e:
|
||
status_label.config(text=f"❌ Ошибка: {str(e)}", fg="#f44336")
|
||
|
||
# Кнопки
|
||
btn_frame = tk.Frame(dialog)
|
||
btn_frame.pack(pady=20)
|
||
|
||
tk.Button(btn_frame, text="Деплоить", bg="#4CAF50", fg="white",
|
||
command=deploy, width=15).pack(side=tk.LEFT, padx=10)
|
||
|
||
tk.Button(btn_frame, text="Отмена", bg="#f44336", fg="white",
|
||
command=dialog.destroy, width=15).pack(side=tk.LEFT, padx=10)
|
||
|
||
def test_contract_function(self, function_name: str, *args):
|
||
"""Тестирование функции контракта"""
|
||
if not self.selected_component or self.selected_component.type != "solidity":
|
||
return
|
||
|
||
component = self.selected_component
|
||
|
||
if not component.deployed or not component.contract_address:
|
||
messagebox.showwarning("Ошибка", "Контракт не деплоен")
|
||
return
|
||
|
||
try:
|
||
# Создаем деплоер для тестирования
|
||
deployer = Web3Deployer(component.network_url, component.chain_id)
|
||
|
||
if not deployer.is_connected():
|
||
messagebox.showerror("Ошибка", "Нет подключения к сети")
|
||
return
|
||
|
||
# Тестируем функцию
|
||
result = deployer.test_contract_function(
|
||
contract_address=component.contract_address,
|
||
abi=component.abi,
|
||
function_name=function_name,
|
||
args=args
|
||
)
|
||
|
||
if result['success']:
|
||
if result['type'] == 'call':
|
||
messagebox.showinfo("Результат",
|
||
f"Функция {function_name} вызвана успешно\n"
|
||
f"Результат: {result['result']}\n"
|
||
f"Контракт: {component.contract_address}")
|
||
else:
|
||
messagebox.showinfo("Информация",
|
||
f"Функция {function_name} требует транзакцию\n"
|
||
f"{result['message']}\n\n"
|
||
f"Для вызова этой функции нужен приватный ключ.")
|
||
else:
|
||
messagebox.showerror("Ошибка", f"Ошибка вызова функции: {result['error']}")
|
||
|
||
except Exception as e:
|
||
messagebox.showerror("Ошибка", f"Ошибка тестирования: {str(e)}")
|
||
|
||
def copy_to_clipboard(self, text: str):
|
||
"""Копирование текста в буфер обмена"""
|
||
self.root.clipboard_clear()
|
||
self.root.clipboard_append(text)
|
||
self.update_status("Скопировано в буфер обмена")
|
||
|
||
def generate_wallet(self):
|
||
"""Генерация нового кошелька"""
|
||
dialog = tk.Toplevel(self.root)
|
||
dialog.title("Генератор кошелька")
|
||
dialog.geometry("600x300")
|
||
dialog.resizable(False, False)
|
||
|
||
tk.Label(dialog, text="Новый кошелек Ethereum",
|
||
font=('Arial', 14, 'bold')).pack(pady=20)
|
||
|
||
# Генерируем ключи
|
||
private_key = "0x" + secrets.token_hex(32)
|
||
account = Account.from_key(private_key)
|
||
|
||
# Приватный ключ
|
||
tk.Label(dialog, text="Приватный ключ:",
|
||
font=('Arial', 10, 'bold')).pack(anchor='w', padx=20, pady=(10, 0))
|
||
|
||
priv_frame = tk.Frame(dialog)
|
||
priv_frame.pack(fill=tk.X, padx=20, pady=5)
|
||
|
||
priv_var = tk.StringVar(value=private_key)
|
||
priv_entry = tk.Entry(priv_frame, textvariable=priv_var,
|
||
font=('Consolas', 9), state='readonly', width=70)
|
||
priv_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||
|
||
tk.Button(priv_frame, text="📋", bg="#2196F3", fg="white",
|
||
command=lambda: self.copy_to_clipboard(private_key)).pack(side=tk.RIGHT)
|
||
|
||
# Адрес
|
||
tk.Label(dialog, text="Адрес кошелька:",
|
||
font=('Arial', 10, 'bold')).pack(anchor='w', padx=20, pady=(10, 0))
|
||
|
||
addr_frame = tk.Frame(dialog)
|
||
addr_frame.pack(fill=tk.X, padx=20, pady=5)
|
||
|
||
addr_var = tk.StringVar(value=account.address)
|
||
addr_entry = tk.Entry(addr_frame, textvariable=addr_var,
|
||
font=('Consolas', 9), state='readonly', width=70)
|
||
addr_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||
|
||
tk.Button(addr_frame, text="📋", bg="#2196F3", fg="white",
|
||
command=lambda: self.copy_to_clipboard(account.address)).pack(side=tk.RIGHT)
|
||
|
||
# Предупреждение
|
||
warning_frame = tk.Frame(dialog, bg="#FFF3CD", bd=1, relief=tk.SUNKEN)
|
||
warning_frame.pack(fill=tk.X, padx=20, pady=20)
|
||
|
||
tk.Label(warning_frame, text="⚠️ ВНИМАНИЕ!",
|
||
bg="#FFF3CD", fg="#856404", font=('Arial', 10, 'bold')).pack(anchor='w', padx=10, pady=(10, 0))
|
||
|
||
tk.Label(warning_frame, text="Сохраните приватный ключ в безопасном месте!",
|
||
bg="#FFF3CD", fg="#856404", wraplength=550).pack(anchor='w', padx=10, pady=(0, 10))
|
||
|
||
tk.Label(warning_frame, text="Никогда не используйте этот ключ в основной сети (Mainnet)!",
|
||
bg="#FFF3CD", fg="#856404", wraplength=550).pack(anchor='w', padx=10, pady=(0, 10))
|
||
|
||
# Кнопка закрытия
|
||
tk.Button(dialog, text="Закрыть", bg="#4CAF50", fg="white",
|
||
command=dialog.destroy, width=20).pack(pady=20)
|
||
|
||
def test_network(self):
|
||
"""Тестируем подключение к сети"""
|
||
# Используем сеть из выбранного компонента или по умолчанию
|
||
network_url = "http://localhost:8545"
|
||
chain_id = 1337
|
||
|
||
if self.selected_component and self.selected_component.type == "solidity":
|
||
network_url = self.selected_component.network_url
|
||
chain_id = self.selected_component.chain_id
|
||
|
||
dialog = tk.Toplevel(self.root)
|
||
dialog.title("Тест сети")
|
||
dialog.geometry("500x500")
|
||
|
||
tk.Label(dialog, text="Тестирование подключения к сети",
|
||
font=('Arial', 12, 'bold')).pack(pady=20)
|
||
|
||
# Ввод URL сети
|
||
frame = tk.Frame(dialog)
|
||
frame.pack(fill=tk.X, padx=20, pady=10)
|
||
|
||
tk.Label(frame, text="URL сети:").pack(side=tk.LEFT)
|
||
network_url_entry = tk.Entry(frame)
|
||
network_url_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=10)
|
||
network_url_entry.insert(0, network_url)
|
||
|
||
# Chain ID
|
||
chain_frame = tk.Frame(dialog)
|
||
chain_frame.pack(fill=tk.X, padx=20, pady=10)
|
||
|
||
tk.Label(chain_frame, text="Chain ID:").pack(side=tk.LEFT)
|
||
chain_id_entry = tk.Entry(chain_frame)
|
||
chain_id_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=10)
|
||
chain_id_entry.insert(0, str(chain_id))
|
||
|
||
result_text = scrolledtext.ScrolledText(dialog, height=15, font=('Consolas', 9))
|
||
result_text.pack(fill=tk.BOTH, expand=True, padx=20, pady=10)
|
||
|
||
def test_connection():
|
||
try:
|
||
url = network_url_entry.get()
|
||
chain = int(chain_id_entry.get())
|
||
|
||
result_text.delete(1.0, tk.END)
|
||
result_text.insert(tk.END, f"Подключение к: {url}\n")
|
||
result_text.insert(tk.END, f"Chain ID: {chain}\n")
|
||
result_text.insert(tk.END, "="*50 + "\n\n")
|
||
|
||
deployer = Web3Deployer(url, chain)
|
||
|
||
if deployer.is_connected():
|
||
result_text.insert(tk.END, "✓ Подключение успешно\n\n")
|
||
|
||
# Информация о сети
|
||
w3 = deployer.w3
|
||
result_text.insert(tk.END, f"Версия сети: {w3.net.version}\n")
|
||
result_text.insert(tk.END, f"Последний блок: {w3.eth.block_number}\n")
|
||
result_text.insert(tk.END, f"Газ цена: {w3.eth.gas_price} wei\n")
|
||
result_text.insert(tk.END, f"Сложность: {w3.eth.get_block('latest').get('difficulty', 'N/A')}\n")
|
||
|
||
# Аккаунты (для локальных сетей)
|
||
try:
|
||
accounts = w3.eth.accounts
|
||
if accounts:
|
||
result_text.insert(tk.END, f"\nАккаунты в сети: {len(accounts)}\n")
|
||
for i, acc in enumerate(accounts[:5]):
|
||
balance = w3.eth.get_balance(acc)
|
||
result_text.insert(tk.END, f" {i}: {acc[:20]}... - {w3.from_wei(balance, 'ether')} ETH\n")
|
||
if len(accounts) > 5:
|
||
result_text.insert(tk.END, f" ... и еще {len(accounts)-5}\n")
|
||
except Exception as e:
|
||
result_text.insert(tk.END, f"\nНе удалось получить аккаунты: {e}\n")
|
||
|
||
result_text.insert(tk.END, "\n" + "="*50 + "\n")
|
||
result_text.insert(tk.END, "✅ Сеть готова к работе!\n")
|
||
result_text.insert(tk.END, "Рекомендации:\n")
|
||
result_text.insert(tk.END, "1. Убедитесь что у аккаунта есть ETH для газа\n")
|
||
result_text.insert(tk.END, "2. Проверьте Chain ID\n")
|
||
result_text.insert(tk.END, "3. Для Ganache используйте Chain ID = 1337\n")
|
||
|
||
else:
|
||
result_text.insert(tk.END, "✗ Не удалось подключиться к сети\n\n")
|
||
result_text.insert(tk.END, "Убедитесь что:\n")
|
||
result_text.insert(tk.END, "1. Ganache запущен (для localhost:8545)\n")
|
||
result_text.insert(tk.END, "2. Правильный URL сети\n")
|
||
result_text.insert(tk.END, "3. Сеть доступна\n")
|
||
result_text.insert(tk.END, "4. Chain ID указан верно\n")
|
||
|
||
except ValueError:
|
||
result_text.insert(tk.END, "Ошибка: Chain ID должен быть числом\n")
|
||
except Exception as e:
|
||
result_text.insert(tk.END, f"Ошибка: {str(e)}\n")
|
||
|
||
tk.Button(dialog, text="Тестировать", bg="#2196F3", fg="white",
|
||
command=test_connection).pack(pady=10)
|
||
|
||
def insert_example_code(self, editor):
|
||
"""Вставляем пример кода"""
|
||
examples = {
|
||
"SimpleStorage": """// SPDX-License-Identifier: MIT
|
||
pragma solidity ^0.8.0;
|
||
|
||
contract SimpleStorage {
|
||
uint256 private storedData;
|
||
|
||
constructor(uint256 initialValue) {
|
||
storedData = initialValue;
|
||
}
|
||
|
||
function set(uint256 x) public {
|
||
storedData = x;
|
||
}
|
||
|
||
function get() public view returns (uint256) {
|
||
return storedData;
|
||
}
|
||
}""",
|
||
|
||
"Token": """// SPDX-License-Identifier: MIT
|
||
pragma solidity ^0.8.0;
|
||
|
||
contract MyToken {
|
||
string public name = "MyToken";
|
||
string public symbol = "MTK";
|
||
uint8 public decimals = 18;
|
||
uint256 public totalSupply;
|
||
|
||
mapping(address => uint256) public balanceOf;
|
||
mapping(address => mapping(address => uint256)) public allowance;
|
||
|
||
event Transfer(address indexed from, address indexed to, uint256 value);
|
||
event Approval(address indexed owner, address indexed spender, uint256 value);
|
||
|
||
constructor(uint256 initialSupply) {
|
||
totalSupply = initialSupply * 10 ** uint256(decimals);
|
||
balanceOf[msg.sender] = totalSupply;
|
||
}
|
||
|
||
function transfer(address to, uint256 value) public returns (bool) {
|
||
require(balanceOf[msg.sender] >= value, "Insufficient balance");
|
||
balanceOf[msg.sender] -= value;
|
||
balanceOf[to] += value;
|
||
emit Transfer(msg.sender, to, value);
|
||
return true;
|
||
}
|
||
}""",
|
||
|
||
"Voting": """// SPDX-License-Identifier: MIT
|
||
pragma solidity ^0.8.0;
|
||
|
||
contract Voting {
|
||
struct Proposal {
|
||
string name;
|
||
uint voteCount;
|
||
}
|
||
|
||
Proposal[] public proposals;
|
||
mapping(address => bool) public hasVoted;
|
||
|
||
constructor(string[] memory proposalNames) {
|
||
for (uint i = 0; i < proposalNames.length; i++) {
|
||
proposals.push(Proposal({
|
||
name: proposalNames[i],
|
||
voteCount: 0
|
||
}));
|
||
}
|
||
}
|
||
|
||
function vote(uint proposal) public {
|
||
require(!hasVoted[msg.sender], "Already voted");
|
||
require(proposal < proposals.length, "Invalid proposal");
|
||
|
||
proposals[proposal].voteCount++;
|
||
hasVoted[msg.sender] = true;
|
||
}
|
||
|
||
function winningProposal() public view returns (uint winningProposal_) {
|
||
uint winningVoteCount = 0;
|
||
for (uint p = 0; p < proposals.length; p++) {
|
||
if (proposals[p].voteCount > winningVoteCount) {
|
||
winningVoteCount = proposals[p].voteCount;
|
||
winningProposal_ = p;
|
||
}
|
||
}
|
||
}
|
||
}"""
|
||
}
|
||
|
||
# Диалог выбора примера
|
||
example_dialog = tk.Toplevel(self.root)
|
||
example_dialog.title("Выберите пример контракта")
|
||
example_dialog.geometry("300x200")
|
||
|
||
for name, code in examples.items():
|
||
btn = tk.Button(example_dialog, text=name, width=20,
|
||
command=lambda c=code, d=example_dialog: (editor.delete(1.0, tk.END),
|
||
editor.insert(1.0, c),
|
||
d.destroy()))
|
||
btn.pack(pady=5)
|
||
|
||
def open_solidity_compiler(self):
|
||
"""Открываем отдельный компилятор Solidity"""
|
||
dialog = tk.Toplevel(self.root)
|
||
dialog.title("Компилятор Solidity")
|
||
dialog.geometry("900x700")
|
||
|
||
# Редактор кода
|
||
editor_frame = tk.Frame(dialog)
|
||
editor_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||
|
||
tk.Label(editor_frame, text="Введите код Solidity:",
|
||
font=('Arial', 11, 'bold')).pack(anchor='w')
|
||
|
||
editor = scrolledtext.ScrolledText(editor_frame, wrap=tk.WORD, font=('Consolas', 10))
|
||
editor.pack(fill=tk.BOTH, expand=True, pady=5)
|
||
editor.insert(1.0, """// SPDX-License-Identifier: MIT
|
||
pragma solidity ^0.8.0;
|
||
|
||
contract TestContract {
|
||
string public message = "Hello, Web3!";
|
||
|
||
function setMessage(string memory newMessage) public {
|
||
message = newMessage;
|
||
}
|
||
|
||
function getMessage() public view returns (string memory) {
|
||
return message;
|
||
}
|
||
}""")
|
||
|
||
# Панель результатов
|
||
result_frame = tk.Frame(dialog)
|
||
result_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||
|
||
notebook = ttk.Notebook(result_frame)
|
||
notebook.pack(fill=tk.BOTH, expand=True)
|
||
|
||
# Вкладка ABI
|
||
abi_frame = tk.Frame(notebook)
|
||
notebook.add(abi_frame, text="ABI")
|
||
abi_text = scrolledtext.ScrolledText(abi_frame, wrap=tk.WORD, font=('Consolas', 9))
|
||
abi_text.pack(fill=tk.BOTH, expand=True)
|
||
|
||
# Вкладка Bytecode
|
||
bytecode_frame = tk.Frame(notebook)
|
||
notebook.add(bytecode_frame, text="Bytecode")
|
||
bytecode_text = scrolledtext.ScrolledText(bytecode_frame, wrap=tk.WORD, font=('Consolas', 9))
|
||
bytecode_text.pack(fill=tk.BOTH, expand=True)
|
||
|
||
# Вкладка Ошибки
|
||
error_frame = tk.Frame(notebook)
|
||
notebook.add(error_frame, text="Ошибки")
|
||
error_text = scrolledtext.ScrolledText(error_frame, wrap=tk.WORD, font=('Consolas', 9))
|
||
error_text.pack(fill=tk.BOTH, expand=True)
|
||
|
||
def compile_code():
|
||
try:
|
||
source_code = editor.get(1.0, tk.END)
|
||
compiled_sol = compile_source(
|
||
source_code,
|
||
output_values=['abi', 'bin'],
|
||
solc_version='0.8.0'
|
||
)
|
||
|
||
# Очищаем предыдущие результаты
|
||
abi_text.delete(1.0, tk.END)
|
||
bytecode_text.delete(1.0, tk.END)
|
||
error_text.delete(1.0, tk.END)
|
||
|
||
for contract_id, contract_interface in compiled_sol.items():
|
||
abi_text.insert(tk.END, f"// {contract_id}\n")
|
||
abi_text.insert(tk.END, json.dumps(contract_interface['abi'], indent=2))
|
||
abi_text.insert(tk.END, "\n\n")
|
||
|
||
bytecode_text.insert(tk.END, f"// {contract_id}\n")
|
||
bytecode_text.insert(tk.END, contract_interface['bin'])
|
||
bytecode_text.insert(tk.END, "\n\n")
|
||
|
||
error_text.insert(tk.END, "✓ Компиляция успешна!\n")
|
||
|
||
except Exception as e:
|
||
error_text.delete(1.0, tk.END)
|
||
error_text.insert(tk.END, f"Ошибка компиляции:\n{str(e)}")
|
||
|
||
# Кнопки
|
||
btn_frame = tk.Frame(dialog)
|
||
btn_frame.pack(fill=tk.X, padx=10, pady=10)
|
||
|
||
tk.Button(btn_frame, text="🛠️ Скомпилировать", bg="#2196F3", fg="white",
|
||
command=compile_code).pack(side=tk.LEFT, padx=5)
|
||
|
||
tk.Button(btn_frame, text="Сохранить как компонент", bg="#4CAF50", fg="white",
|
||
command=lambda: self.save_from_compiler(editor.get(1.0, tk.END))).pack(side=tk.RIGHT, padx=5)
|
||
|
||
def save_from_compiler(self, source_code):
|
||
"""Сохраняем код из компилятора как компонент"""
|
||
comp_id = f"solidity_{len(self.components) + 1}"
|
||
component = SolidityComponent(
|
||
comp_id, "Новый контракт",
|
||
100, 100 + len(self.components) * 50,
|
||
600, 400
|
||
)
|
||
component.source_code = source_code
|
||
component.contract_name = "NewContract"
|
||
|
||
self.components.append(component)
|
||
self.draw_solidity_component(component)
|
||
self.select_component(component)
|
||
|
||
messagebox.showinfo("Успех", "Контракт добавлен на холст")
|
||
|
||
def generate_contract(self):
|
||
"""Генератор контрактов"""
|
||
dialog = tk.Toplevel(self.root)
|
||
dialog.title("Генератор контрактов")
|
||
dialog.geometry("600x500")
|
||
|
||
tk.Label(dialog, text="Выберите тип контракта:",
|
||
font=('Arial', 12, 'bold')).pack(pady=20)
|
||
|
||
# Типы контрактов
|
||
contracts_frame = tk.Frame(dialog)
|
||
contracts_frame.pack(fill=tk.BOTH, expand=True, padx=20)
|
||
|
||
contracts = [
|
||
("Simple Storage", "Простое хранилище данных"),
|
||
("ERC20 Token", "Стандартный токен ERC-20"),
|
||
("Voting", "Система голосования"),
|
||
("Auction", "Аукцион")
|
||
]
|
||
|
||
for name, desc in contracts:
|
||
frame = tk.Frame(contracts_frame, relief=tk.RAISED, borderwidth=1)
|
||
frame.pack(fill=tk.X, pady=5)
|
||
|
||
tk.Label(frame, text=name, font=('Arial', 11, 'bold')).pack(anchor='w', padx=10, pady=5)
|
||
tk.Label(frame, text=desc, fg='#666666').pack(anchor='w', padx=10, pady=(0, 5))
|
||
|
||
btn = tk.Button(frame, text="Создать", bg=self.accent_color, fg=self.text_color,
|
||
command=lambda n=name: self.create_contract_template(n, dialog))
|
||
btn.pack(side=tk.RIGHT, padx=10, pady=5)
|
||
|
||
def create_contract_template(self, contract_type, dialog):
|
||
"""Создаем шаблон контракта"""
|
||
templates = {
|
||
"Simple Storage": """// SPDX-License-Identifier: MIT
|
||
pragma solidity ^0.8.0;
|
||
|
||
contract SimpleStorage {
|
||
uint256 private storedData;
|
||
|
||
constructor(uint256 initialValue) {
|
||
storedData = initialValue;
|
||
}
|
||
|
||
function set(uint256 x) public {
|
||
storedData = x;
|
||
}
|
||
|
||
function get() public view returns (uint256) {
|
||
return storedData;
|
||
}
|
||
}""",
|
||
|
||
"ERC20 Token": """// SPDX-License-Identifier: MIT
|
||
pragma solidity ^0.8.0;
|
||
|
||
contract MyToken {
|
||
string public name;
|
||
string public symbol;
|
||
uint8 public decimals;
|
||
uint256 public totalSupply;
|
||
|
||
mapping(address => uint256) public balanceOf;
|
||
mapping(address => mapping(address => uint256)) public allowance;
|
||
|
||
event Transfer(address indexed from, address indexed to, uint256 value);
|
||
event Approval(address indexed owner, address indexed spender, uint256 value);
|
||
|
||
constructor(
|
||
string memory tokenName,
|
||
string memory tokenSymbol,
|
||
uint8 decimalUnits,
|
||
uint256 initialSupply
|
||
) {
|
||
name = tokenName;
|
||
symbol = tokenSymbol;
|
||
decimals = decimalUnits;
|
||
totalSupply = initialSupply * 10 ** uint256(decimals);
|
||
balanceOf[msg.sender] = totalSupply;
|
||
}
|
||
|
||
function transfer(address to, uint256 value) public returns (bool) {
|
||
require(balanceOf[msg.sender] >= value, "Insufficient balance");
|
||
balanceOf[msg.sender] -= value;
|
||
balanceOf[to] += value;
|
||
emit Transfer(msg.sender, to, value);
|
||
return true;
|
||
}
|
||
}""",
|
||
|
||
"Voting": """// SPDX-License-Identifier: MIT
|
||
pragma solidity ^0.8.0;
|
||
|
||
contract Voting {
|
||
struct Proposal {
|
||
string name;
|
||
uint voteCount;
|
||
}
|
||
|
||
Proposal[] public proposals;
|
||
mapping(address => bool) public hasVoted;
|
||
|
||
constructor(string[] memory proposalNames) {
|
||
for (uint i = 0; i < proposalNames.length; i++) {
|
||
proposals.push(Proposal({
|
||
name: proposalNames[i],
|
||
voteCount: 0
|
||
}));
|
||
}
|
||
}
|
||
|
||
function vote(uint proposal) public {
|
||
require(!hasVoted[msg.sender], "Already voted");
|
||
require(proposal < proposals.length, "Invalid proposal");
|
||
|
||
proposals[proposal].voteCount++;
|
||
hasVoted[msg.sender] = true;
|
||
}
|
||
}""",
|
||
|
||
"Auction": """// SPDX-License-Identifier: MIT
|
||
pragma solidity ^0.8.0;
|
||
|
||
contract SimpleAuction {
|
||
address public beneficiary;
|
||
uint public auctionEndTime;
|
||
address public highestBidder;
|
||
uint public highestBid;
|
||
|
||
mapping(address => uint) public pendingReturns;
|
||
|
||
event HighestBidIncreased(address bidder, uint amount);
|
||
event AuctionEnded(address winner, uint amount);
|
||
|
||
constructor(uint _biddingTime) {
|
||
beneficiary = msg.sender;
|
||
auctionEndTime = block.timestamp + _biddingTime;
|
||
}
|
||
|
||
function bid() public payable {
|
||
require(block.timestamp <= auctionEndTime, "Auction already ended");
|
||
require(msg.value > highestBid, "There already is a higher bid");
|
||
|
||
if (highestBid != 0) {
|
||
pendingReturns[highestBidder] += highestBid;
|
||
}
|
||
highestBidder = msg.sender;
|
||
highestBid = msg.value;
|
||
emit HighestBidIncreased(msg.sender, msg.value);
|
||
}
|
||
}"""
|
||
}
|
||
|
||
if contract_type in templates:
|
||
comp_id = f"solidity_{len(self.components) + 1}"
|
||
component = SolidityComponent(
|
||
comp_id, contract_type,
|
||
100, 100 + len(self.components) * 50,
|
||
600, 400
|
||
)
|
||
component.source_code = templates[contract_type]
|
||
component.contract_name = contract_type.replace(" ", "")
|
||
|
||
self.components.append(component)
|
||
self.draw_solidity_component(component)
|
||
self.select_component(component)
|
||
|
||
dialog.destroy()
|
||
messagebox.showinfo("Успех", f"Шаблон '{contract_type}' создан")
|
||
|
||
def new_project(self):
|
||
"""Создаем новый проект"""
|
||
if self.components and not messagebox.askyesno("Новый проект", "Сохранить текущий проект?"):
|
||
return
|
||
|
||
self.components = []
|
||
self.current_file = None
|
||
self.selected_component = None
|
||
|
||
self.canvas.delete("all")
|
||
self.draw_grid()
|
||
self.update_properties_panel()
|
||
|
||
self.add_default_components()
|
||
|
||
def save_project(self):
|
||
"""Сохраняем проект"""
|
||
if self.current_file:
|
||
self._save_to_file(self.current_file)
|
||
else:
|
||
self.save_project_as()
|
||
|
||
def save_project_as(self):
|
||
"""Сохраняем проект как"""
|
||
filename = filedialog.asksaveasfilename(
|
||
defaultextension=".solidityweb",
|
||
filetypes=[("Solidity Web Projects", "*.solidityweb"), ("Все файлы", "*.*")]
|
||
)
|
||
|
||
if filename:
|
||
self._save_to_file(filename)
|
||
self.current_file = filename
|
||
|
||
def _save_to_file(self, filename):
|
||
"""Сохраняем проект в файл"""
|
||
try:
|
||
data = {
|
||
'components': [],
|
||
'version': '1.0',
|
||
'saved_at': datetime.now().isoformat()
|
||
}
|
||
|
||
for comp in self.components:
|
||
data['components'].append(comp.to_dict())
|
||
|
||
with open(filename, 'w', encoding='utf-8') as f:
|
||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||
|
||
messagebox.showinfo("Сохранено", f"Проект сохранен в {filename}")
|
||
|
||
except Exception as e:
|
||
messagebox.showerror("Ошибка", f"Не удалось сохранить: {str(e)}")
|
||
|
||
def open_project(self):
|
||
"""Открываем проект"""
|
||
filename = filedialog.askopenfilename(
|
||
filetypes=[("Solidity Web Projects", "*.solidityweb"), ("Все файлы", "*.*")]
|
||
)
|
||
|
||
if not filename:
|
||
return
|
||
|
||
try:
|
||
with open(filename, 'r', encoding='utf-8') as f:
|
||
data = json.load(f)
|
||
|
||
self.components = []
|
||
self.canvas.delete("all")
|
||
self.draw_grid()
|
||
|
||
for comp_data in data['components']:
|
||
if comp_data['type'] == 'solidity':
|
||
comp = SolidityComponent.from_dict(comp_data)
|
||
self.draw_solidity_component(comp)
|
||
else:
|
||
comp = BasicComponent.from_dict(comp_data)
|
||
self.draw_basic_component(comp)
|
||
|
||
self.components.append(comp)
|
||
|
||
self.current_file = filename
|
||
self.selected_component = None
|
||
self.update_properties_panel()
|
||
|
||
messagebox.showinfo("Открыто", f"Проект загружен из {filename}")
|
||
|
||
except Exception as e:
|
||
messagebox.showerror("Ошибка", f"Не удалось открыть: {str(e)}")
|
||
|
||
def preview_site(self):
|
||
"""Предпросмотр сайта"""
|
||
dialog = tk.Toplevel(self.root)
|
||
dialog.title("Предпросмотр сайта")
|
||
dialog.geometry("1000x700")
|
||
|
||
# Генерируем HTML
|
||
html_content = self._generate_html()
|
||
|
||
# Показываем в текстовом редакторе
|
||
editor = scrolledtext.ScrolledText(dialog, wrap=tk.WORD, font=('Consolas', 10))
|
||
editor.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||
editor.insert(1.0, html_content)
|
||
|
||
# Кнопки
|
||
btn_frame = tk.Frame(dialog)
|
||
btn_frame.pack(fill=tk.X, padx=10, pady=10)
|
||
|
||
tk.Button(btn_frame, text="Копировать HTML", bg="#2196F3", fg="white",
|
||
command=lambda: self.copy_to_clipboard(html_content)).pack(side=tk.LEFT, padx=5)
|
||
|
||
tk.Button(btn_frame, text="Открыть в браузере", bg="#4CAF50", fg="white",
|
||
command=self._open_in_browser).pack(side=tk.RIGHT, padx=5)
|
||
|
||
def export_site(self):
|
||
"""Экспортируем сайт"""
|
||
if not self.components:
|
||
messagebox.showwarning("Ошибка", "Проект пуст")
|
||
return
|
||
|
||
export_dir = filedialog.askdirectory(title="Выберите папку для экспорта")
|
||
|
||
if not export_dir:
|
||
return
|
||
|
||
try:
|
||
site_dir = os.path.join(export_dir, "solidity_website")
|
||
|
||
# Проверяем существование директории
|
||
if os.path.exists(site_dir):
|
||
response = messagebox.askyesno(
|
||
"Подтверждение",
|
||
f"Папка {site_dir} уже существует. Перезаписать?"
|
||
)
|
||
if not response:
|
||
return
|
||
shutil.rmtree(site_dir)
|
||
|
||
os.makedirs(site_dir, exist_ok=True)
|
||
|
||
# Генерируем файлы
|
||
html_content = self._generate_html()
|
||
css_content = self._generate_css()
|
||
js_content = self._generate_js()
|
||
|
||
# Сохраняем файлы
|
||
with open(os.path.join(site_dir, "index.html"), "w", encoding="utf-8") as f:
|
||
f.write(html_content)
|
||
|
||
with open(os.path.join(site_dir, "style.css"), "w", encoding="utf-8") as f:
|
||
f.write(css_content)
|
||
|
||
with open(os.path.join(site_dir, "script.js"), "w", encoding="utf-8") as f:
|
||
f.write(js_content)
|
||
|
||
# Сохраняем контракты
|
||
contracts_dir = os.path.join(site_dir, "contracts")
|
||
os.makedirs(contracts_dir, exist_ok=True)
|
||
|
||
for comp in self.components:
|
||
if comp.type == "solidity":
|
||
contract_file = os.path.join(contracts_dir, f"{comp.contract_name}.sol")
|
||
with open(contract_file, "w", encoding="utf-8") as f:
|
||
f.write(comp.source_code)
|
||
|
||
# Создаем README
|
||
readme_content = self._generate_readme()
|
||
with open(os.path.join(site_dir, "README.md"), "w", encoding="utf-8") as f:
|
||
f.write(readme_content)
|
||
|
||
messagebox.showinfo("Экспорт", f"Сайт экспортирован в:\n{site_dir}")
|
||
|
||
except Exception as e:
|
||
messagebox.showerror("Ошибка", f"Не удалось экспортировать: {str(e)}")
|
||
|
||
def _generate_html(self):
|
||
"""Генерируем HTML"""
|
||
html = """<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Solidity Web3 Сайт</title>
|
||
<link rel="stylesheet" href="style.css">
|
||
<script src="https://cdn.jsdelivr.net/npm/web3@1.7.0/dist/web3.min.js"></script>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
"""
|
||
|
||
for comp in sorted(self.components, key=lambda c: (c.y, c.x)):
|
||
if comp.type == "header":
|
||
html += f"""
|
||
<div class="component header" style="left: {comp.x}px; top: {comp.y}px; width: {comp.width}px; height: {comp.height}px;">
|
||
<h1>{comp.properties.get('text', 'Заголовок')}</h1>
|
||
</div>"""
|
||
|
||
elif comp.type == "paragraph":
|
||
html += f"""
|
||
<div class="component paragraph" style="left: {comp.x}px; top: {comp.y}px; width: {comp.width}px; height: {comp.height}px;">
|
||
<p>{comp.properties.get('text', 'Текст')}</p>
|
||
</div>"""
|
||
|
||
elif comp.type == "button":
|
||
html += f"""
|
||
<div class="component button" style="left: {comp.x}px; top: {comp.y}px; width: {comp.width}px; height: {comp.height}px;">
|
||
<button>{comp.properties.get('text', 'Кнопка')}</button>
|
||
</div>"""
|
||
|
||
elif comp.type == "image":
|
||
html += f"""
|
||
<div class="component image" style="left: {comp.x}px; top: {comp.y}px; width: {comp.width}px; height: {comp.height}px;">
|
||
<img src="{comp.properties.get('src', 'https://placehold.co/400x300')}" alt="{comp.properties.get('alt', 'Изображение')}" style="width:100%;height:100%;object-fit:cover;">
|
||
</div>"""
|
||
|
||
elif comp.type == "solidity":
|
||
status = "не скомпилирован"
|
||
if comp.compiled:
|
||
status = "скомпилирован"
|
||
if comp.deployed:
|
||
status = f"деплоен: {comp.contract_address}"
|
||
|
||
html += f"""
|
||
<div class="component solidity" style="left: {comp.x}px; top: {comp.y}px; width: {comp.width}px; height: {comp.height}px;">
|
||
<div class="contract-header">
|
||
<h3>🛠️ {comp.contract_name}</h3>
|
||
<span class="status">{status}</span>
|
||
</div>
|
||
<div class="contract-actions">
|
||
<button onclick="connectContract('{comp.contract_address}')">Подключиться</button>
|
||
<button onclick="callGet()">get()</button>
|
||
<button onclick="callSet(42)">set(42)</button>
|
||
</div>
|
||
<div id="result-{comp.id}" class="result"></div>
|
||
</div>"""
|
||
|
||
html += """
|
||
</div>
|
||
<script src="script.js"></script>
|
||
</body>
|
||
</html>"""
|
||
|
||
return html
|
||
|
||
def _generate_css(self):
|
||
"""Генерируем CSS"""
|
||
return """* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: Arial, sans-serif;
|
||
background: #1a1a2e;
|
||
color: white;
|
||
min-height: 100vh;
|
||
padding: 20px;
|
||
}
|
||
|
||
.container {
|
||
position: relative;
|
||
width: 1200px;
|
||
height: 800px;
|
||
margin: 0 auto;
|
||
background: #0f3460;
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.component {
|
||
position: absolute;
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
}
|
||
|
||
.header {
|
||
background: linear-gradient(90deg, #0f3460, #1a5f9c);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.header h1 {
|
||
font-size: 2rem;
|
||
text-align: center;
|
||
}
|
||
|
||
.paragraph {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.paragraph p {
|
||
font-size: 1.1rem;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.button {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.button button {
|
||
background: #4CAF50;
|
||
color: white;
|
||
border: none;
|
||
padding: 12px 24px;
|
||
border-radius: 25px;
|
||
font-size: 16px;
|
||
cursor: pointer;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.image {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.image img {
|
||
max-width: 100%;
|
||
max-height: 100%;
|
||
}
|
||
|
||
.solidity {
|
||
background: #1c2541;
|
||
border: 2px solid #5bc0be;
|
||
}
|
||
|
||
.contract-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.contract-header h3 {
|
||
color: #5bc0be;
|
||
}
|
||
|
||
.status {
|
||
background: #2d4059;
|
||
padding: 5px 10px;
|
||
border-radius: 15px;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.contract-actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.contract-actions button {
|
||
background: #4a69bd;
|
||
color: white;
|
||
border: none;
|
||
padding: 8px 16px;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
flex: 1;
|
||
min-width: 100px;
|
||
}
|
||
|
||
.result {
|
||
margin-top: 15px;
|
||
padding: 10px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 5px;
|
||
min-height: 40px;
|
||
}"""
|
||
|
||
def _generate_js(self):
|
||
"""Генерируем JavaScript"""
|
||
return """let web3;
|
||
let contract;
|
||
let currentContractAddress;
|
||
|
||
async function connectContract(address) {
|
||
try {
|
||
if (typeof window.ethereum !== 'undefined') {
|
||
web3 = new Web3(window.ethereum);
|
||
await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||
|
||
// ABI контракта SimpleStorage
|
||
const abi = [
|
||
{
|
||
"inputs": [{"internalType": "uint256","name": "initialValue","type": "uint256"}],
|
||
"stateMutability": "nonpayable",
|
||
"type": "constructor"
|
||
},
|
||
{
|
||
"inputs": [],
|
||
"name": "decrement",
|
||
"outputs": [],
|
||
"stateMutability": "nonpayable",
|
||
"type": "function"
|
||
},
|
||
{
|
||
"inputs": [],
|
||
"name": "get",
|
||
"outputs": [{"internalType": "uint256","name": "","type": "uint256"}],
|
||
"stateMutability": "view",
|
||
"type": "function"
|
||
},
|
||
{
|
||
"inputs": [],
|
||
"name": "increment",
|
||
"outputs": [],
|
||
"stateMutability": "nonpayable",
|
||
"type": "function"
|
||
},
|
||
{
|
||
"inputs": [{"internalType": "uint256","name": "x","type": "uint256"}],
|
||
"name": "set",
|
||
"outputs": [],
|
||
"stateMutability": "nonpayable",
|
||
"type": "function"
|
||
}
|
||
];
|
||
|
||
if (address && address.startsWith('0x')) {
|
||
currentContractAddress = address;
|
||
contract = new web3.eth.Contract(abi, address);
|
||
alert('Контракт подключен: ' + address);
|
||
} else {
|
||
alert('Введите действительный адрес контракта');
|
||
}
|
||
} else {
|
||
alert('Установите MetaMask для работы с контрактами');
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка подключения:', error);
|
||
alert('Ошибка подключения к контракту');
|
||
}
|
||
}
|
||
|
||
async function callGet() {
|
||
if (!contract) {
|
||
alert('Сначала подключите контракт');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await contract.methods.get().call();
|
||
document.querySelector('.result').textContent = 'Результат get(): ' + result;
|
||
} catch (error) {
|
||
console.error('Ошибка вызова get:', error);
|
||
alert('Ошибка вызова функции');
|
||
}
|
||
}
|
||
|
||
async function callSet(value) {
|
||
if (!contract || !web3) {
|
||
alert('Сначала подключите контракт');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const accounts = await web3.eth.getAccounts();
|
||
await contract.methods.set(value).send({ from: accounts[0] });
|
||
document.querySelector('.result').textContent = 'Функция set(' + value + ') выполнена';
|
||
} catch (error) {
|
||
console.error('Ошибка вызова set:', error);
|
||
alert('Ошибка вызова функции');
|
||
}
|
||
}
|
||
|
||
// Автоподключение при загрузке
|
||
window.addEventListener('DOMContentLoaded', () {
|
||
if (typeof window.ethereum !== 'undefined') {
|
||
web3 = new Web3(window.ethereum);
|
||
}
|
||
});"""
|
||
|
||
def _generate_readme(self):
|
||
"""Генерация README файла"""
|
||
contracts = [comp.contract_name for comp in self.components if comp.type == "solidity"]
|
||
contracts_list = "\n".join([f"- {name}" for name in contracts])
|
||
|
||
return f"""# Solidity Web3 Сайт
|
||
|
||
Сгенерировано с помощью Solidity Web3 Builder
|
||
|
||
## Содержимое проекта:
|
||
|
||
1. `index.html` - Главная страница
|
||
2. `style.css` - Стили
|
||
3. `script.js` - JavaScript с Web3 логикой
|
||
4. `contracts/` - Исходные коды контрактов
|
||
|
||
## Контракты в проекте:
|
||
|
||
{contracts_list if contracts_list else "Нет контрактов"}
|
||
|
||
## Инструкция по запуску:
|
||
|
||
1. Установите MetaMask браузерное расширение
|
||
2. Подключитесь к тестовой сети (Goerli, Sepolia) или локальной сети (Ganache)
|
||
3. Откройте index.html в браузере
|
||
4. Подключите кошелек
|
||
|
||
## Требования для деплоя:
|
||
|
||
1. Установите Ganache для локального тестирования
|
||
2. Импортируйте приватный ключ в Ganache
|
||
3. Убедитесь что у аккаунта есть ETH для газа
|
||
|
||
## Безопасность:
|
||
|
||
- Никогда не используйте приватные ключи от реальных кошельков
|
||
- Тестируйте только на тестовых сетях
|
||
- Проверяйте контракты на уязвимости перед использованием
|
||
|
||
---
|
||
|
||
Сгенерировано: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||
"""
|
||
|
||
def _open_in_browser(self):
|
||
"""Открываем в браузере"""
|
||
try:
|
||
# Создаем временный файл
|
||
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as f:
|
||
f.write(self._generate_html())
|
||
temp_file = f.name
|
||
|
||
# Открываем в браузере
|
||
webbrowser.open(f'file://{temp_file}')
|
||
|
||
except Exception as e:
|
||
messagebox.showerror("Ошибка", f"Не удалось открыть в браузере: {str(e)}")
|
||
|
||
def show_docs(self):
|
||
"""Показываем документацию"""
|
||
dialog = tk.Toplevel(self.root)
|
||
dialog.title("Документация")
|
||
dialog.geometry("700x500")
|
||
|
||
text = scrolledtext.ScrolledText(dialog, wrap=tk.WORD, font=('Arial', 10))
|
||
text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||
|
||
docs = """
|
||
=== Solidity Web3 Builder - Документация ===
|
||
|
||
1. ОСНОВНЫЕ ВОЗМОЖНОСТИ:
|
||
|
||
• Создание сайтов с поддержкой Web3
|
||
• Редактирование и компиляция Solidity контрактов
|
||
• Визуальное размещение компонентов
|
||
• Реальный деплой контрактов в сети
|
||
• Экспорт готового сайта
|
||
|
||
2. РАБОТА С КОНТРАКТАМИ:
|
||
|
||
• Редактирование кода в встроенном редакторе
|
||
• Компиляция через py-solc-x
|
||
• Реальный деплой с использованием приватных ключей
|
||
• Тестирование функций контракта
|
||
|
||
3. БЕЗОПАСНОСТЬ:
|
||
|
||
• Приватные ключи запрашиваются только при деплое
|
||
• Ключи не сохраняются в проекте
|
||
• Поддержка PoA middleware для Ganache
|
||
• Генератор тестовых кошельков
|
||
|
||
4. ТРЕБОВАНИЯ ДЛЯ ДЕПЛОЯ:
|
||
|
||
• Ganache (рекомендуется) для локального тестирования
|
||
• Приватный ключ аккаунта с ETH для газа
|
||
• Chain ID сети (1337 для Ganache)
|
||
• URL сети (http://localhost:8545 для Ganache)
|
||
|
||
5. БЫСТРЫЙ СТАРТ:
|
||
|
||
1. Запустите Ganache на localhost:8545
|
||
2. Создайте компонент "Контракт Solidity"
|
||
3. Отредактируйте код контракта
|
||
4. Нажмите "Скомпилировать"
|
||
5. Нажмите "Деплоить" и введите приватный ключ
|
||
6. Протестируйте функции
|
||
7. Экспортируйте сайт
|
||
|
||
=== Важные замечания ===
|
||
|
||
• Используйте только тестовые приватные ключи!
|
||
• Ganache Chain ID = 1337
|
||
• Для деплоя нужен ETH на аккаунте
|
||
• Все транзакции реальные (в выбранной сети)
|
||
"""
|
||
|
||
text.insert(1.0, docs)
|
||
text.config(state=tk.DISABLED)
|
||
|
||
def show_examples(self):
|
||
"""Показываем примеры контрактов"""
|
||
dialog = tk.Toplevel(self.root)
|
||
dialog.title("Примеры контрактов")
|
||
dialog.geometry("800x600")
|
||
|
||
notebook = ttk.Notebook(dialog)
|
||
notebook.pack(fill=tk.BOTH, expand=True)
|
||
|
||
examples = {
|
||
"Простое хранилище": """// SPDX-License-Identifier: MIT
|
||
pragma solidity ^0.8.0;
|
||
|
||
contract SimpleStorage {
|
||
uint256 private value;
|
||
|
||
function set(uint256 newValue) public {
|
||
value = newValue;
|
||
}
|
||
|
||
function get() public view returns (uint256) {
|
||
return value;
|
||
}
|
||
}""",
|
||
|
||
"Голосование": """// SPDX-License-Identifier: MIT
|
||
pragma solidity ^0.8.0;
|
||
|
||
contract Voting {
|
||
struct Proposal {
|
||
string name;
|
||
uint voteCount;
|
||
}
|
||
|
||
Proposal[] public proposals;
|
||
mapping(address => bool) public hasVoted;
|
||
|
||
constructor(string[] memory proposalNames) {
|
||
for (uint i = 0; i < proposalNames.length; i++) {
|
||
proposals.push(Proposal({
|
||
name: proposalNames[i],
|
||
voteCount: 0
|
||
}));
|
||
}
|
||
}
|
||
|
||
function vote(uint proposal) public {
|
||
require(!hasVoted[msg.sender], "Already voted");
|
||
require(proposal < proposals.length, "Invalid proposal");
|
||
|
||
proposals[proposal].voteCount++;
|
||
hasVoted[msg.sender] = true;
|
||
}
|
||
}""",
|
||
|
||
"Краудфандинг": """// SPDX-License-Identifier: MIT
|
||
pragma solidity ^0.8.0;
|
||
|
||
contract Crowdfunding {
|
||
address public creator;
|
||
uint public goal;
|
||
uint public deadline;
|
||
uint public totalFunds;
|
||
mapping(address => uint) public contributions;
|
||
|
||
event Funded(address donor, uint amount);
|
||
event GoalReached(uint total);
|
||
|
||
constructor(uint _goal, uint _duration) {
|
||
creator = msg.sender;
|
||
goal = _goal;
|
||
deadline = block.timestamp + _duration;
|
||
}
|
||
|
||
function contribute() public payable {
|
||
require(block.timestamp < deadline, "Campaign ended");
|
||
require(msg.value > 0, "Contribution must be positive");
|
||
|
||
contributions[msg.sender] += msg.value;
|
||
totalFunds += msg.value;
|
||
|
||
emit Funded(msg.sender, msg.value);
|
||
|
||
if (totalFunds >= goal) {
|
||
emit GoalReached(totalFunds);
|
||
}
|
||
}
|
||
}"""
|
||
}
|
||
|
||
for title, code in examples.items():
|
||
frame = tk.Frame(notebook)
|
||
notebook.add(frame, text=title)
|
||
|
||
text = scrolledtext.ScrolledText(frame, wrap=tk.WORD, font=('Consolas', 10))
|
||
text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||
text.insert(1.0, code)
|
||
|
||
def show_about(self):
|
||
"""О программе"""
|
||
dialog = tk.Toplevel(self.root)
|
||
dialog.title("О программе")
|
||
dialog.geometry("400x300")
|
||
|
||
tk.Label(dialog, text="Solidity Web3 Builder",
|
||
font=('Arial', 16, 'bold')).pack(pady=20)
|
||
|
||
tk.Label(dialog, text="Версия 2.0",
|
||
font=('Arial', 12)).pack(pady=10)
|
||
|
||
tk.Label(dialog, text="Конструктор сайтов с реальной поддержкой\nсмарт-контрактов Solidity",
|
||
justify=tk.CENTER).pack(pady=10)
|
||
|
||
tk.Label(dialog, text="Возможности:",
|
||
font=('Arial', 11, 'bold')).pack(pady=(20, 5))
|
||
|
||
features = [
|
||
"• Редактирование Solidity кода",
|
||
"• Реальный деплой контрактов",
|
||
"• Поддержка web3.py 7.x",
|
||
"• Экспорт готовых Web3 сайтов"
|
||
]
|
||
|
||
for feature in features:
|
||
tk.Label(dialog, text=feature).pack()
|
||
|
||
tk.Label(dialog, text="\nДля работы требуется:",
|
||
font=('Arial', 10)).pack(pady=(10, 0))
|
||
|
||
tk.Label(dialog, text="Python 3.8+, web3, py-solc-x, eth-account",
|
||
fg='#666666').pack()
|
||
|
||
|
||
def main():
|
||
"""Точка входа"""
|
||
try:
|
||
root = tk.Tk()
|
||
app = SolidityBuilder(root)
|
||
root.mainloop()
|
||
except Exception as e:
|
||
messagebox.showerror("Критическая ошибка", f"Программа завершилась с ошибкой:\n{str(e)}")
|
||
sys.exit(1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|