diff --git a/main.py b/main.py new file mode 100644 index 0000000..6bfb0be --- /dev/null +++ b/main.py @@ -0,0 +1,2797 @@ +""" +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('', 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("", self.on_canvas_click) + self.canvas.bind("", self.on_canvas_drag) + self.canvas.bind("", self.on_canvas_release) + self.canvas.bind("", 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 = """ + + + + + Solidity Web3 Сайт + + + + +
+""" + + for comp in sorted(self.components, key=lambda c: (c.y, c.x)): + if comp.type == "header": + html += f""" +
+

{comp.properties.get('text', 'Заголовок')}

+
""" + + elif comp.type == "paragraph": + html += f""" +
+

{comp.properties.get('text', 'Текст')}

+
""" + + elif comp.type == "button": + html += f""" +
+ +
""" + + elif comp.type == "image": + html += f""" +
+ {comp.properties.get('alt', 'Изображение')} +
""" + + elif comp.type == "solidity": + status = "не скомпилирован" + if comp.compiled: + status = "скомпилирован" + if comp.deployed: + status = f"деплоен: {comp.contract_address}" + + html += f""" +
+
+

🛠️ {comp.contract_name}

+ {status} +
+
+ + + +
+
+
""" + + 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()