git / moonbite-network / gui.py · raw
"""MyCoin Desktop GUI Application using PyQt6.A user-friendly interface for MyCoin mining and wallet management.This is an educational project and does not hold real funds."""import sysimport timefrom threading import Threadfrom typing import Optionalfrom PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTabWidget, QTextEdit, QLabel, QPushButton, QSpinBox, QProgressBar,)from PyQt6.QtCore import Qt, pyqtSignal, QObject, QThreadfrom PyQt6.QtGui import QFontfrom node import Node# Wallet is used by MainWindow further down; importing only HDWallet left# it undefined, so the window failed to build with a NameError.from wallet import HDWallet, Wallet# Signal emitter for thread-safe GUI updates from mining threadsclass MiningWorker(QObject): """Emits progress signals during mining.""" progress = pyqtSignal(int, str, int) # blocks_mined, block_hash, total_utxo finished = pyqtSignal() error = pyqtSignal(str) def __init__(self, node: Node, miner_address: str, num_blocks: int): super().__init__() self.node = node self.miner_address = miner_address self.num_blocks = num_blocks self.should_stop = False def run(self): """Mine `num_blocks` blocks and emit progress.""" try: for i in range(self.num_blocks): if self.should_stop: break # Mine one block block = self.node.mine_block(self.miner_address) if block is None: self.error.emit(f"Failed to mine block {i + 1}") break # Emit progress block_hash = block.hash total_utxo = self.node.chain.utxo.total_value() self.progress.emit(i + 1, block_hash, total_utxo) self.finished.emit() except Exception as e: self.error.emit(str(e))class WalletTab(QWidget): """Tab 1: Wallet management with HD wallet (BIP39/BIP32) support.""" def __init__(self, node: Node): super().__init__() self.node = node self.wallet = HDWallet() self.current_address = "" self.init_ui() def init_ui(self): layout = QVBoxLayout() # Title title = QLabel("HD Wallet Management (BIP39/BIP32)") title.setFont(QFont("Arial", 12, QFont.Weight.Bold)) layout.addWidget(title) # Seed backup section layout.addWidget(QLabel("Backup Seed Phrase (WRITE IT DOWN!):")) self.seed_display = QTextEdit() self.seed_display.setReadOnly(True) self.seed_display.setMaximumHeight(80) self.seed_display.setText(self.wallet.export_seed()) layout.addWidget(self.seed_display) # Seed backup button backup_btn = QPushButton("Copy Seed to Clipboard") backup_btn.clicked.connect(self.copy_seed_to_clipboard) layout.addWidget(backup_btn) # Current address section layout.addWidget(QLabel("Current Address:")) self.address_display = QTextEdit() self.address_display.setReadOnly(True) self.address_display.setMaximumHeight(60) layout.addWidget(self.address_display) # Generate new key button self.gen_key_btn = QPushButton("Generate Next Address") self.gen_key_btn.clicked.connect(self.generate_new_key) layout.addWidget(self.gen_key_btn) # Check balance section self.balance_btn = QPushButton("Check Balance") self.balance_btn.clicked.connect(self.check_balance) layout.addWidget(self.balance_btn) self.balance_display = QLabel("Balance: -- coins, -- cents") self.balance_display.setFont(QFont("Arial", 11)) layout.addWidget(self.balance_display) # Transaction history layout.addWidget(QLabel("Recent Transaction History:")) self.tx_history = QTextEdit() self.tx_history.setReadOnly(True) layout.addWidget(self.tx_history) layout.addStretch() self.setLayout(layout) def copy_seed_to_clipboard(self): """Copy seed phrase to clipboard for backup.""" from PyQt6.QtWidgets import QApplication clipboard = QApplication.clipboard() clipboard.setText(self.wallet.export_seed()) def generate_new_key(self): """Generate next HD-derived address (m/44'/0'/0'/0/n).""" addr = self.wallet.new_key() index = self.wallet._derivation_index - 1 self.current_address = addr path = f"m/44'/0'/0'/0/{index}" self.address_display.setText(f"{addr}\n(Bech32 - Path: {path})") self.balance_display.setText("Balance: -- coins, -- cents") def check_balance(self): """Query and display the wallet balance from the UTXO set.""" if not self.current_address: self.balance_display.setText("Balance: No address generated yet") return utxos = list(self.node.chain.utxo.items()) balance_cents = self.wallet.balance(utxos) coins = balance_cents // 100_000_000 cents = balance_cents % 100_000_000 self.balance_display.setText(f"Balance: {coins} coins, {cents} cents") # Update transaction history display history_text = "" for entry in self.wallet.history: history_text += ( f"TxID: {entry['txid'][:16]}...\n" f" To: {entry['to'][:16]}...\n" f" Amount: {entry['amount']} cents\n\n" ) if history_text: self.tx_history.setText(history_text) else: self.tx_history.setText("No transactions yet.") def refresh_display(self): """Refresh displayed address and balance.""" if self.current_address: self.address_display.setText(f"{self.current_address}\n(Base58Check)")class MiningTab(QWidget): """Tab 2: Mining control.""" def __init__(self, node: Node, wallet: HDWallet): super().__init__() self.node = node self.wallet = wallet self.mining_thread: Optional[QThread] = None self.mining_worker: Optional[MiningWorker] = None self.total_blocks = 1 self.init_ui() def init_ui(self): layout = QVBoxLayout() # Title title = QLabel("Mining Control") title.setFont(QFont("Arial", 12, QFont.Weight.Bold)) layout.addWidget(title) # Number of blocks spinbox hlayout = QHBoxLayout() hlayout.addWidget(QLabel("Number of Blocks:")) self.blocks_spinbox = QSpinBox() self.blocks_spinbox.setMinimum(1) self.blocks_spinbox.setMaximum(1000) self.blocks_spinbox.setValue(1) hlayout.addWidget(self.blocks_spinbox) hlayout.addStretch() layout.addLayout(hlayout) # Start mining button self.start_mining_btn = QPushButton("Start Mining") self.start_mining_btn.clicked.connect(self.start_mining) layout.addWidget(self.start_mining_btn) # Stop mining button (initially disabled) self.stop_mining_btn = QPushButton("Stop Mining") self.stop_mining_btn.setEnabled(False) self.stop_mining_btn.clicked.connect(self.stop_mining) layout.addWidget(self.stop_mining_btn) # Progress bar layout.addWidget(QLabel("Progress:")) self.progress_bar = QProgressBar() self.progress_bar.setMinimum(0) self.progress_bar.setMaximum(100) self.progress_bar.setValue(0) layout.addWidget(self.progress_bar) # Status label self.status_label = QLabel("Status: Ready") self.status_label.setFont(QFont("Arial", 10)) layout.addWidget(self.status_label) # Info display self.info_display = QTextEdit() self.info_display.setReadOnly(True) layout.addWidget(self.info_display) layout.addStretch() self.setLayout(layout) def start_mining(self): """Start mining N blocks in a separate thread.""" if not self.wallet.addresses: self.status_label.setText("Status: Error - No wallet address generated") return self.total_blocks = self.blocks_spinbox.value() miner_address = self.wallet.addresses[-1] self.start_mining_btn.setEnabled(False) self.stop_mining_btn.setEnabled(True) self.blocks_spinbox.setEnabled(False) self.progress_bar.setValue(0) # Create and start mining worker in a thread self.mining_worker = MiningWorker(self.node, miner_address, self.total_blocks) self.mining_thread = QThread() self.mining_worker.moveToThread(self.mining_thread) # Connect signals self.mining_thread.started.connect(self.mining_worker.run) self.mining_worker.progress.connect(self.on_mining_progress) self.mining_worker.finished.connect(self.on_mining_finished) self.mining_worker.error.connect(self.on_mining_error) self.status_label.setText(f"Status: Mining {self.total_blocks} blocks...") self.mining_thread.start() def stop_mining(self): """Stop the current mining operation.""" if self.mining_worker: self.mining_worker.should_stop = True self.status_label.setText("Status: Stopping...") def on_mining_progress(self, blocks_mined: int, block_hash: str, total_utxo: int): """Update UI with mining progress.""" progress_pct = (blocks_mined / self.total_blocks) * 100 self.progress_bar.setValue(int(progress_pct)) height = self.node.chain.height timestamp = self.node.chain.tip_block().header.timestamp info = ( f"Blocks Mined: {blocks_mined} / {self.total_blocks}\n" f"Current Height: {height}\n" f"Latest Hash: {block_hash[:16]}...\n" f"Total UTXO: {total_utxo} cents\n" f"Timestamp: {timestamp}" ) self.info_display.setText(info) self.status_label.setText(f"Status: Mined {blocks_mined} blocks...") def on_mining_finished(self): """Handle mining completion.""" self.progress_bar.setValue(100) self.status_label.setText("Status: Mining completed") self.start_mining_btn.setEnabled(True) self.stop_mining_btn.setEnabled(False) self.blocks_spinbox.setEnabled(True) if self.mining_thread: self.mining_thread.quit() self.mining_thread.wait() def on_mining_error(self, error_msg: str): """Handle mining errors.""" self.status_label.setText(f"Status: Error - {error_msg}") self.start_mining_btn.setEnabled(True) self.stop_mining_btn.setEnabled(False) self.blocks_spinbox.setEnabled(True) if self.mining_thread: self.mining_thread.quit() self.mining_thread.wait()class BlockchainTab(QWidget): """Tab 3: Blockchain information.""" def __init__(self, node: Node): super().__init__() self.node = node self.init_ui() def init_ui(self): layout = QVBoxLayout() # Title title = QLabel("Blockchain Information") title.setFont(QFont("Arial", 12, QFont.Weight.Bold)) layout.addWidget(title) # Info display self.info_display = QTextEdit() self.info_display.setReadOnly(True) layout.addWidget(self.info_display) # Refresh button self.refresh_btn = QPushButton("Refresh") self.refresh_btn.clicked.connect(self.refresh_info) layout.addWidget(self.refresh_btn) layout.addStretch() self.setLayout(layout) # Initial refresh self.refresh_info() def refresh_info(self): """Fetch and display blockchain info.""" genesis_hash = self.node.chain.genesis_hash tip_hash = self.node.chain.tip height = self.node.chain.height total_utxo = self.node.chain.utxo.total_value() num_blocks = len(self.node.chain.blocks) num_transactions = sum( len(self.node.chain.blocks[h].transactions) for h in self.node.chain.blocks ) info_text = ( f"Genesis Hash:\n {genesis_hash}\n\n" f"Current Tip Hash:\n {tip_hash}\n\n" f"Chain Height: {height}\n" f"Total UTXO Value: {total_utxo} cents\n" f"Number of Blocks: {num_blocks}\n" f"Number of Transactions: {num_transactions}" ) self.info_display.setText(info_text)class SettingsTab(QWidget): """Tab 4: Application settings.""" def __init__(self, node: Node): super().__init__() self.node = node self.init_ui() def init_ui(self): layout = QVBoxLayout() # Title title = QLabel("Settings") title.setFont(QFont("Arial", 12, QFont.Weight.Bold)) layout.addWidget(title) # Coinbase maturity setting hlayout = QHBoxLayout() hlayout.addWidget(QLabel("Coinbase Maturity (blocks):")) self.maturity_spinbox = QSpinBox() self.maturity_spinbox.setMinimum(1) self.maturity_spinbox.setMaximum(1000) self.maturity_spinbox.setValue(self.node.chain.coinbase_maturity) hlayout.addWidget(self.maturity_spinbox) hlayout.addStretch() layout.addLayout(hlayout) # Save settings button self.save_btn = QPushButton("Save Settings") self.save_btn.clicked.connect(self.save_settings) layout.addWidget(self.save_btn) # Status self.status_label = QLabel("Settings saved") self.status_label.setFont(QFont("Arial", 10)) layout.addWidget(self.status_label) # Debug/logs area layout.addWidget(QLabel("Debug Logs:")) self.logs_display = QTextEdit() self.logs_display.setReadOnly(True) layout.addWidget(self.logs_display) layout.addStretch() self.setLayout(layout) self.log_message(f"Coinbase maturity: {self.node.chain.coinbase_maturity}") self.log_message(f"Node ID: {self.node.node_id}") def save_settings(self): """Save settings (note: maturity affects future blocks, not retroactively).""" new_maturity = self.maturity_spinbox.value() self.node.chain.coinbase_maturity = new_maturity self.status_label.setText(f"Settings saved (maturity={new_maturity})") self.log_message(f"Coinbase maturity updated to {new_maturity}") def log_message(self, msg: str): """Append a message to the logs display.""" current = self.logs_display.toPlainText() timestamp = time.strftime("%Y-%m-%d %H:%M:%S") self.logs_display.setText(f"{current}{timestamp}: {msg}\n")class MainWindow(QMainWindow): """Main application window with tabbed interface.""" def __init__(self, node: Node): super().__init__() self.node = node self.wallet_tab = None self.mining_tab = None self.init_ui() def init_ui(self): self.setWindowTitle("MyCoin Mining & Wallet Manager") self.setGeometry(100, 100, 900, 700) # Central widget with tabs central_widget = QWidget() layout = QVBoxLayout(central_widget) # Create wallet (shared between tabs) wallet = Wallet() # Create tabs tabs = QTabWidget() # Tab 1: Wallet self.wallet_tab = WalletTab(self.node) self.wallet_tab.wallet = wallet tabs.addTab(self.wallet_tab, "Wallet") # Tab 2: Mining self.mining_tab = MiningTab(self.node, wallet) tabs.addTab(self.mining_tab, "Mining") # Tab 3: Blockchain blockchain_tab = BlockchainTab(self.node) tabs.addTab(blockchain_tab, "Blockchain") # Tab 4: Settings settings_tab = SettingsTab(self.node) tabs.addTab(settings_tab, "Settings") layout.addWidget(tabs) central_widget.setLayout(layout) self.setCentralWidget(central_widget) # Menu bar menubar = self.menuBar() # File menu file_menu = menubar.addMenu("File") exit_action = file_menu.addAction("Exit") exit_action.triggered.connect(self.close) # Help menu help_menu = menubar.addMenu("Help") about_action = help_menu.addAction("About") about_action.triggered.connect(self.show_about) def show_about(self): """Display about dialog.""" from PyQt6.QtWidgets import QMessageBox QMessageBox.information( self, "About MyCoin GUI", "MyCoin Mining & Wallet Manager\n\n" "An educational desktop application for mining and managing MyCoin.\n" "This application does not hold real funds.\n\n" "Built with PyQt6", )def main(): """Main entry point.""" # Create Node instance node = Node("gui-app", coinbase_maturity=100) # Create QApplication app = QApplication(sys.argv) # Create and show main window window = MainWindow(node) window.show() sys.exit(app.exec())if __name__ == "__main__": main()