diff --git a/README.md b/README.md
index 34faefa..3915311 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,12 @@
-# Bitcoin Improvement Proposal - 0038 / BIP38
+
+
Bitcoin Improvement Proposal - 0038
+
+
+
+ Releases · Manual · API Docs · Donation
+
+
+
[![Build Status](https://img.shields.io/github/actions/workflow/status/meherett/python-bip38/build.yml)](https://github.com/meherett/python-bip38/actions/workflows/build.yml)
[![PyPI Version](https://img.shields.io/pypi/v/bip38.svg?color=blue)](https://pypi.org/project/bip38)
@@ -7,10 +15,14 @@
[![PyPI Python Version](https://img.shields.io/pypi/pyversions/bip38.svg)](https://pypi.org/project/bip38)
[![Coverage Status](https://coveralls.io/repos/github/meherett/python-bip38/badge.svg?branch=master)](https://coveralls.io/github/meherett/python-bip38)
+
+
A Python library for the implementation of Bitcoin Improvement Proposal - 0038 / (BIP38) protocol.
This library supports both [No EC-multiply](https://github.com/bitcoin/bips/blob/master/bip-0038.mediawiki#encryption-when-ec-multiply-flag-is-not-used) and [EC-multiply](https://github.com/bitcoin/bips/blob/master/bip-0038.mediawiki#encryption-when-ec-multiply-mode-is-used) modes and is compatible with over 150+ cryptocurrencies.
It's specifically tailored for Pay-to-PubKey-Hash (P2PKH) address types.
+![Desktop Application](docs/static/gif/bip38.gif)
+
For more info see the [Passphrase-protected private key - BIP38](https://en.bitcoin.it/wiki/BIP_0038) spec.
## Installation
@@ -417,7 +429,7 @@ BIP38 Decrypted: {
To get started, just fork this repo, clone it locally, and run:
```
-pip install -e .[tests,docs]
+pip install -e .[desktop,tests,docs]
```
## Testing
@@ -450,9 +462,8 @@ This module supports more than 150+ cryptocurrencies, including the following:
Buy me a coffee if You found this tool helpful:
-- **Bitcoin** - 12uaGVdX1t86FXLQ4yYPrRQDCK7xGGu82r
-- **Ethereum / Tether** - 0xCCAad7A87fd81553d0F93F743Fb4Fc6B213b228B
-- **Bitcoin / Ethereum / Tether** - With Unstoppable [hd.wallet](https://ud.me/hd.wallet)
+- **Bitcoin** - 16c7ajUwHEMaafrceuYSrd35SDjmfVdjoS
+- **Ethereum / ERC20** - 0xD3cbCB0B6F82A03C715D665b72dC44CEf54e6D9B
Thank you very much for your support.
diff --git a/build.py b/build.py
new file mode 100644
index 0000000..b646cb0
--- /dev/null
+++ b/build.py
@@ -0,0 +1,89 @@
+#!/usr/bin/env python3
+
+# Copyright © 2024, Eyoel Tadesse
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or https://opensource.org/license/mit
+
+from cx_Freeze import setup, Executable
+
+import platform
+
+from desktop.info import __version__ as version
+from bip38.info import __author__ as author
+
+app_name = "BIP38"
+
+if platform.system() == "Windows":
+ icon_path = "desktop/ui/images/icon/icon.ico"
+else:
+ icon_path = "desktop/ui/images/svg/logo.svg"
+
+msi_shortcut_table = [
+ (
+ "DesktopShortcut", # Shortcut
+ "DesktopFolder", # Directory_
+ app_name, # Name that will be show on the link
+ "TARGETDIR", # Component_
+ f"[TARGETDIR]{app_name}.exe", # Target exe to exexute
+ None, # Arguments
+ None, # Description
+ None, # Hotkey
+ None, # Icon
+ None, # IconIndex
+ None, # ShowCmd
+ 'TARGETDIR' # WkDir
+ )
+]
+
+msi_directory_table = [
+ ("ProgramMenuFolder", "TARGETDIR", "."),
+ ("BIP38Menu", "ProgramMenuFolder", "BIP38~1|BIP38")
+]
+
+msi_data = {
+ "Shortcut": msi_shortcut_table,
+ "Directory": msi_directory_table
+}
+
+bdist_msi_opt = {
+ "add_to_path": False,
+ "data": msi_data,
+ "initial_target_dir": f"[ProgramFiles64Folder]\\{app_name}",
+ "install_icon": icon_path,
+ "upgrade_code": "{E4A369F6-FC76-3013-A420-3BB9B370711C}"
+}
+
+build_exe_opt = {
+ "packages": ["_scrypt"],
+ "excludes": ["tkinter", "PySide6.QtNetwork", "PySide6.translations"],
+ "bin_excludes": [
+ "Qt6Network.dll", "Qt6OpenGL.dll", "Qt6Pdf.dll", "Qt6Qml.dll", "Qt6QmlMeta.dll",
+ "Qt6QmlModels.dll", "Qt6QmlWorkerScript.dll", "Qt6Quick.dll", "Qt6VirtualKeyboard.dll",
+ "qgif.dll", "qicns.dll", "qjpeg.dll", "qpdf.dll", "qtga.dll", "qtiff.dll", "qwbmp.dll", "qwebp.dll",
+ "qtvirtualkeyboardplugin.dll", "qtuiotouchplugin.dll", "qdirect2d.dll", "qoffscreen.dll", "qminimal.dll"
+ ],
+ "include_msvcr": True
+}
+
+executables = [
+ Executable(
+ "launch.py",
+ base="gui",
+ icon=icon_path,
+ target_name=app_name,
+ shortcut_name=app_name,
+ shortcut_dir="BIP38Menu",
+ copyright=f"Copyright (C) 2024 {app_name}"
+ )
+]
+
+setup(
+ name=app_name,
+ author=author,
+ version=version,
+ executables=executables,
+ options={
+ "build_exe": build_exe_opt,
+ "bdist_msi": bdist_msi_opt
+ }
+)
\ No newline at end of file
diff --git a/desktop/__init__.py b/desktop/__init__.py
new file mode 100644
index 0000000..b6e3e62
--- /dev/null
+++ b/desktop/__init__.py
@@ -0,0 +1,5 @@
+#!/usr/bin/env python3
+
+# Copyright © 2024, Eyoel Tadesse
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or https://opensource.org/license/mit
\ No newline at end of file
diff --git a/desktop/core.py b/desktop/core.py
new file mode 100644
index 0000000..a79ab2f
--- /dev/null
+++ b/desktop/core.py
@@ -0,0 +1,115 @@
+#!/usr/bin/env python3
+
+# Copyright © 2024, Eyoel Tadesse
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or https://opensource.org/license/mit
+
+from typing import Optional
+
+import json
+
+from PySide6.QtWidgets import (
+ QMainWindow, QWidget, QStackedWidget,
+ QFrame, QComboBox
+)
+from PySide6.QtCore import (
+ Qt, QFileSystemWatcher, Signal, QSize
+)
+from PySide6.QtGui import (
+ QFontDatabase, QIcon
+)
+
+from desktop.utils import (
+ resolve_path, put_svg
+)
+from desktop.ui.ui_bip38 import Ui_MainWindow
+from desktop.info import __version__ as desktop_ver
+from bip38.info import __version__ as library_ver
+
+class Application(QMainWindow):
+ _instance: Optional['Application'] = None
+ ui: Ui_MainWindow = None
+ theme_watcher: QFileSystemWatcher = None
+ resized: Signal = Signal(object)
+
+ def __new__(cls, *args, **kwargs) -> 'Application':
+ """
+ Create a new instance if not already created, implementing the Singleton pattern.
+ """
+ if not cls._instance:
+ cls._instance = super().__new__(cls)
+ return cls._instance
+
+ def __init__(self) -> None:
+ """
+ Initialize the Application instance if not already initialized.
+ """
+ if not hasattr(self, 'initialized'):
+ super().__init__()
+ self.initialize()
+ self.initialized = True
+
+ @classmethod
+ def instance(cls) -> 'Application':
+ """
+ Get the singleton instance of the Application.
+
+ :return: The singleton instance of Application.
+ """
+ if not cls._instance:
+ cls._instance = cls()
+ return cls._instance
+
+ def initialize(self) -> None:
+ """
+ Perform initialization tasks for the application, such as setting up the UI and loading resources.
+ """
+ self.ui = Ui_MainWindow()
+ self.ui.setupUi(self)
+
+ self.detached_window = None
+
+ self.setWindowTitle("Bitcoin Improvement Proposal - 0038")
+ self.bip38_icon = QIcon(resolve_path("desktop/ui/images/icon/icon.ico"))
+ self.setWindowIcon(self.bip38_icon)
+
+ put_svg(
+ self.ui.bip38LogoHLayout,
+ resolve_path("desktop/ui/images/svg/full-logo.svg"),
+ 84,
+ 44
+ )
+ self.ui.bip38LogoHLayout.setContentsMargins(0, 0, 5, 0)
+
+ css_path = resolve_path("desktop/ui/css/theme.css")
+ self.theme_watcher = QFileSystemWatcher([css_path])
+ self.theme_watcher.fileChanged.connect(lambda: self.load_stylesheet(css_path))
+ QFontDatabase.addApplicationFont(resolve_path("desktop/ui/font/HDWallet.ttf"))
+ self.load_stylesheet(css_path)
+
+ info = {
+ "library": library_ver,
+ "desktop": desktop_ver
+ }
+ self.ui.outputQTextEdit.setPlaceholderText(json.dumps(info, indent=4))
+
+ # remove combo box shadow
+ for qComboBox in self.findChildren(QComboBox):
+ qComboBox.findChild(QFrame).setWindowFlags(Qt.Popup | Qt.NoDropShadowWindowHint)
+
+ def load_stylesheet(self, path: str) -> None:
+ """
+ Load and apply a stylesheet from the specified path.
+
+ :param path: The path to the stylesheet file.
+ """
+ try:
+ with open(path, 'r', encoding='utf-8') as style_file:
+ stylesheet = style_file.read()
+ self.setStyleSheet(stylesheet)
+ except Exception as e:
+ print(f"Failed to load stylesheet: {e}")
+
+ def resizeEvent(self, event) -> None:
+ super().resizeEvent(event)
+ self.resized.emit(event)
\ No newline at end of file
diff --git a/desktop/info.py b/desktop/info.py
new file mode 100644
index 0000000..1b3339d
--- /dev/null
+++ b/desktop/info.py
@@ -0,0 +1,7 @@
+#!/usr/bin/env python3
+
+# Copyright © 2024, Eyoel Tadesse
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or https://opensource.org/license/mit
+
+__version__: str = "v0.1.0"
\ No newline at end of file
diff --git a/desktop/main.py b/desktop/main.py
new file mode 100644
index 0000000..db54c26
--- /dev/null
+++ b/desktop/main.py
@@ -0,0 +1,533 @@
+#!/usr/bin/env python3
+
+# Copyright © 2024, Eyoel Tadesse
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or https://opensource.org/license/mit
+
+import re
+import os
+import json
+import inspect
+import functools
+from typing import (
+ Optional, Union
+)
+
+from PySide6.QtCore import QRegularExpression
+from PySide6.QtWidgets import (
+ QWidget, QPushButton
+)
+from PySide6.QtGui import (
+ QTextCharFormat, QTextCursor, QColor,
+ QTextFormat, QTextOption, QRegularExpressionValidator
+)
+
+from bip38 import (
+ cryptocurrencies, BIP38
+)
+from bip38.exceptions import (
+ Error, PassphraseError, WIFError
+)
+from bip38.cryptocurrencies import ICryptocurrency
+from bip38.wif import (
+ get_wif_type, private_key_to_wif
+)
+
+from desktop.core import Application
+
+
+str_to_int = lambda s: int(s) if s.isdigit() else None
+
+class BIP38Application:
+ """
+ Main application class for managing the UI and core functionalities.
+ """
+ TEXT_COLOR: QColor = QColor(255, 255, 255)
+ ERROR_COLOR: QColor = QColor(255, 96, 96)
+ JSON_PATTERNS = [
+ (re.compile(r'\".*?\"(?=\s*:)'), QColor(101, 97, 156)), # keys
+ (re.compile(r'\".*?\"'), QColor(255, 255, 255)), # string
+ (re.compile(r'\b\d+\b'), QColor(125, 211, 252)), # numbers
+ (re.compile(r'\btrue\b|\bfalse\b|\bnull\b'), QColor(21, 128, 61)), # boolean/null
+ (re.compile(r'[{}[\],:]'), QColor(255, 255, 255)) # punctuation
+ ]
+
+ def __init__(self) -> None:
+ """
+ Initialize the MainApplication class.
+ """
+ super().__init__()
+
+ self.app: Application = Application.instance()
+ self.ui = self.app.ui
+
+ # bip38 cryptocurrencies {name: class} dict
+ self.cryptocurrencies: dict = {
+ name: cls for name, cls in inspect.getmembers(cryptocurrencies, inspect.isclass)
+ if issubclass(cls, ICryptocurrency)
+ }
+ del self.cryptocurrencies["ICryptocurrency"] # Remove Interface class from the dict
+
+ self.modes: dict = {
+ "No EC-Multiply": self.ui.noECQWidget,
+ "EC-Multiply": self.ui.ecQWidget,
+ }
+
+ self.wif_types = ["wif", "wif-compressed"]
+
+ self.inputs = {
+ "Passphrase": {
+ "input": self.ui.passphraseQLineEdit,
+ "optional": False, # cant be empty
+ "min_length": 1
+ },
+ "Private Key": {
+ "input": self.ui.noECPrivateKeyQLineEdit,
+ "validator": self.regex_validator("^[0-9A-Fa-f]{1,64}$"), # hex only
+ "optional": False,
+ "min_length": 64
+ },
+ "WIF": {
+ "input": self.ui.noECWIFQLineEdit,
+ "validator": self.regex_validator("^[0-9A-Za-z]{1,52}$"), # alpha num only
+ "optional": False,
+ "min_length": 51
+ },
+ "Owner Salt": {
+ "input": self.ui.ecOwnerSaltQLineEdit,
+ "validator": self.regex_validator("^[0-9A-Fa-f]{1,16}$"), # hex only
+ "optional": False,
+ "min_length": 16
+ },
+ "Seed": {
+ "input": self.ui.ecSeedQLineEdit,
+ "validator": self.regex_validator("^[0-9A-Fa-f]{1,48}$"), # hex only
+ "optional": False,
+ "min_length": 48
+ },
+ "Lot": {
+ "input": self.ui.ecLotQLineEdit,
+ "optional": True,
+ "min_length": 6
+ },
+ "Sequence": {
+ "input": self.ui.ecSequenceQLineEdit,
+ "optional": True,
+ "min_length": 1
+ },
+ "Intermediate Passphrase": {
+ "input": self.ui.ecIPassphraseQLineEdit,
+ "validator": self.regex_validator("^passphrase[0-9A-Za-z]{1,62}$"), # alpha num only with passphrase prefix
+ "optional": False,
+ "min_length": 72
+ },
+ "Confirmation Code": {
+ "input": self.ui.ecConfirmCodeQLineEdit,
+ "validator": self.regex_validator("^cfrm[0-9A-Za-z]{1,71}$"), # alpha num only with passphrase cfrm
+ "optional": False,
+ "min_length": 75
+ },
+ "Encrypted WIF": {
+ "input": self.ui.decryptWIFQLineEdit,
+ "validator": self.regex_validator("^[0-9A-Za-z]{1,58}$"), # alpha num only
+ "optional": False,
+ "min_length": 58
+ }
+ }
+
+ self.__init_ui()
+
+ def __init_ui(self) -> None:
+ """
+ Initialize the UI components and their connections.
+ """
+
+ self.ui.outputQTextEdit.setWordWrapMode(QTextOption.NoWrap)
+
+ ## populate and bindings
+ self.ui.cryptocurrencyQComboBox.addItems(
+ sorted(self.cryptocurrencies.keys(), key=str.casefold)
+ )
+ self.ui.cryptocurrencyQComboBox.currentTextChanged.connect(self.change_cryptocurrency)
+
+ self.ui.modeQComboBox.addItems(
+ self.modes.keys()
+ )
+ self.ui.modeQComboBox.currentTextChanged.connect(self.change_mode)
+
+ self.ui.noECWIFTypeQComboBox.addItems(self.wif_types)
+ self.ui.noECWIFTypeQComboBox.setCurrentIndex(0)
+
+ self.ui.createEncryptedWIFTypeQComboBox.addItems(self.wif_types)
+ self.ui.createEncryptedWIFTypeQComboBox.setCurrentIndex(0)
+
+ # validation stuff
+
+ for key, item in self.inputs.items():
+ qt_input = item["input"]
+
+ if "validator" in item:
+ qt_input.setValidator(item["validator"])
+ qt_input.textChanged.connect(functools.partial(self.validate_input, key))
+
+ self.ui.ecLotQLineEdit.textEdited.connect(
+ lambda: self.validate_int(self.ui.ecLotQLineEdit)
+ )
+ self.ui.ecLotQLineEdit.editingFinished.connect(
+ lambda: self.enforce_int_range(self.ui.ecLotQLineEdit, 100000, 999999)
+ )
+
+ self.ui.ecSequenceQLineEdit.textEdited.connect(
+ lambda: self.validate_int(self.ui.ecSequenceQLineEdit)
+ )
+ self.ui.ecSequenceQLineEdit.editingFinished.connect(
+ lambda: self.enforce_int_range(self.ui.ecSequenceQLineEdit, 0, 4095)
+ )
+
+ # button bindings
+ self.ui.noECPrivateKeyConvertQPushButton.clicked.connect(self.noec_convert_private_key)
+ self.ui.noECEncryptQPushButton.clicked.connect(self.noec_encrypt)
+
+ self.ui.ecOwnerSaltGenerateQPushButton.clicked.connect(
+ lambda: self.ui.ecOwnerSaltQLineEdit.setText(
+ os.urandom(8).hex()
+ )
+ )
+ self.ui.ecSeedGenerateQPushButton.clicked.connect(
+ lambda: self.ui.ecSeedQLineEdit.setText(
+ os.urandom(24).hex()
+ )
+ )
+
+ self.ui.ecIPassphraseGenerateQPushButton.clicked.connect(self.ec_generate_ipassphrase)
+ self.ui.ecConfirmCodeVerifyQPushButton.clicked.connect(self.ec_confirm_code)
+ self.ui.createEncryptedWIFQPushButton.clicked.connect(self.create_encrypted_wif)
+
+ self.ui.decryptWIFQPushButton.clicked.connect(self.decrypt)
+
+ ## setting default values
+
+ # will update network combo too
+ self.ui.cryptocurrencyQComboBox.setCurrentText("Bitcoin")
+ self.ui.modeQComboBox.setCurrentIndex(0)
+
+ # Custom components
+
+ self.clear_button = QPushButton("Clear", self.app)
+ self.clear_button.setFixedWidth(60)
+ self.clear_button.clicked.connect(lambda: self.ui.outputQTextEdit.setText(None))
+ self.clear_button.clicked.connect(self.clean_all_required)
+
+ self.app.resized.connect(self.update_clear_button_position)
+ self.ui.outputQTextEdit.verticalScrollBar().rangeChanged.connect(self.update_clear_button_position)
+
+ def update_clear_button_position(self) -> None:
+ button_width = self.clear_button.sizeHint().width()
+ button_height = self.clear_button.sizeHint().height()
+ window_width = self.app.width()
+ window_height = self.app.height()
+
+ right_margin = 15
+ bottom_margin = 10
+ right_margin += 5 if self.ui.outputQTextEdit.verticalScrollBar().maximum() > 0 else 0
+
+ self.clear_button.move(window_width - button_width - right_margin, window_height - button_height - bottom_margin)
+
+ def change_cryptocurrency(self, cryptocurrency: str) -> None:
+ cryptocurrency_class: ICryptocurrency = self.cryptocurrencies[cryptocurrency]
+
+ self.ui.networkQComboBox.clear()
+ self.ui.networkQComboBox.addItems(
+ [network.title() for network in cryptocurrency_class.NETWORKS.keys()]
+ )
+ self.ui.networkQComboBox.setCurrentIndex(0)
+
+ def change_mode(self, mode: str) -> None:
+ is_on_ec = "EC-Multiply" == mode
+ self.ui.decryptWIFTypeContainerQFrame.setVisible(is_on_ec)
+ self.ui.createEncryptedWIFQPushButton.setVisible(is_on_ec)
+ self.ui.modeQStackedWidget.setCurrentWidget(self.modes[mode])
+ self.clean_all_required()
+
+ def cryptocurrency(self):
+ cryptocurrency_name: str = self.ui.cryptocurrencyQComboBox.currentText()
+ cryptocurrency: ICryptocurrency = self.cryptocurrencies[cryptocurrency_name]
+ return cryptocurrency
+
+ def network(self):
+ return self.ui.networkQComboBox.currentText().lower()
+
+
+ def bip38(self):
+ return BIP38(
+ cryptocurrency=self.cryptocurrency(),
+ network=self.network()
+ )
+
+ def noec_convert_private_key(self):
+ try:
+ private_key, = self.validate_and_get("Private Key")
+
+ wif: str = private_key_to_wif(
+ private_key=private_key,
+ cryptocurrency=self.cryptocurrency(),
+ network=self.network(),
+ wif_type=self.ui.noECWIFTypeQComboBox.currentText()
+ )
+ self.ui.noECWIFQLineEdit.setText(wif)
+
+ output = {
+ "cryptocurrency": self.ui.cryptocurrencyQComboBox.currentText(),
+ "network": self.network(),
+ "wif_type": self.ui.noECWIFTypeQComboBox.currentText(),
+ "private_key": private_key,
+ "wif": wif
+ }
+ self.log(output)
+
+ except (Error, BIP38Application.ValidationError) as e:
+ self.logerr(e)
+
+ def noec_encrypt(self):
+ try:
+ wif, passphrase = self.validate_and_get("WIF", "Passphrase")
+
+ encrypted_wif: str = self.bip38().encrypt(
+ wif=wif,
+ passphrase=passphrase
+ )
+ self.ui.decryptWIFQLineEdit.setText(encrypted_wif)
+
+ wif_type = get_wif_type(wif=wif, cryptocurrency=self.cryptocurrency(), network=self.network())
+
+ output = {
+ "cryptocurrency": self.ui.cryptocurrencyQComboBox.currentText(),
+ "network": self.network(),
+ "passphrase": passphrase,
+ "wif": wif,
+ "wif_type": wif_type,
+ "encrypted_wif": encrypted_wif
+ }
+ self.log(output)
+ except WIFError as we:
+ self.set_required(self.ui.noECWIFQLineEdit, True)
+ self.logerr(we)
+ except (Error, BIP38Application.ValidationError) as e:
+ self.logerr(e)
+
+ def ec_generate_ipassphrase(self):
+ try:
+
+ passphrase, owner_salt, lot, sequence = self.validate_and_get(
+ "Passphrase", "Owner Salt", "Lot", "Sequence"
+ )
+
+ lot: Optional[int] = str_to_int(lot)
+ sequence: Optional[int] = str_to_int(sequence)
+
+ if (lot and sequence is None) or (lot is None and sequence):
+ self.set_required(self.ui.ecLotQLineEdit, True)
+ self.set_required(self.ui.ecSequenceQLineEdit, True)
+ raise BIP38Application.ValidationError("Lot and Sequence must both be set or both left blank.")
+
+ intermediate_passphrase: str = self.bip38().intermediate_code(
+ passphrase=passphrase,
+ owner_salt=owner_salt,
+ lot=lot,
+ sequence=sequence
+ )
+ self.ui.ecIPassphraseQLineEdit.setText(intermediate_passphrase)
+
+ output = {
+ "cryptocurrency": self.ui.cryptocurrencyQComboBox.currentText(),
+ "network": self.network(),
+ "passphrase": passphrase,
+ "owner_salt": owner_salt,
+ "lot": lot,
+ "sequence": sequence,
+ "intermediate_passphrase": intermediate_passphrase
+ }
+ self.log(output)
+ except (Error, BIP38Application.ValidationError) as e:
+ self.logerr(e)
+
+ def ec_confirm_code(self):
+ try:
+ passphrase, confirmation_code = self.validate_and_get(
+ "Passphrase", "Confirmation Code"
+ )
+
+ confirmation_code = self.bip38().confirm_code(
+ passphrase=passphrase,
+ confirmation_code=confirmation_code,
+ detail=True
+ )
+ self.log(confirmation_code)
+
+ except PassphraseError as pe:
+ self.set_required(self.ui.passphraseQLineEdit, True)
+ self.logerr(pe)
+ except Error as e:
+ self.set_required(self.ui.ecConfirmCodeQLineEdit, True)
+ self.logerr(e)
+ except BIP38Application.ValidationError as ve:
+ self.logerr(ve)
+
+ def create_encrypted_wif(self):
+ try:
+ seed, intermediate_passphrase = self.validate_and_get(
+ "Seed", "Intermediate Passphrase"
+ )
+
+ wif_type: str = self.ui.createEncryptedWIFTypeQComboBox.currentText()
+
+ encrypted_wif: str = self.bip38().create_new_encrypted_wif(
+ intermediate_passphrase=intermediate_passphrase,
+ wif_type=wif_type,
+ seed=seed,
+ )
+ self.ui.decryptWIFQLineEdit.setText(encrypted_wif["encrypted_wif"])
+ self.log(encrypted_wif)
+
+ except PassphraseError as pe:
+ self.set_required(self.ui.ecIPassphraseQLineEdit, True)
+ self.logerr(pe)
+ except (Error, BIP38Application.ValidationError) as e:
+ self.logerr(e)
+
+ def decrypt(self):
+ try:
+ encrypted_wif, passphrase = self.validate_and_get("Encrypted WIF", "Passphrase")
+ decrypted_wif: dict = self.bip38().decrypt(
+ encrypted_wif=encrypted_wif,
+ passphrase=passphrase,
+ detail=True
+ )
+ self.log(decrypted_wif)
+ except WIFError as we:
+ self.set_required(self.ui.decryptWIFQLineEdit, True)
+ self.logerr(we)
+ except PassphraseError as pe:
+ self.set_required(self.ui.passphraseQLineEdit, True)
+ self.logerr(pe)
+ except (Error, BIP38Application.ValidationError) as e:
+ self.logerr(e)
+
+ def log(self, data: Optional[Union[str, dict]], end="\n") -> None:
+ if isinstance(data, dict):
+ data = json.dumps(data, indent=4)
+
+ cursor = self.ui.outputQTextEdit.textCursor()
+ cursor.movePosition(QTextCursor.End)
+
+ default_format = QTextCharFormat()
+ default_format.setForeground(BIP38Application.TEXT_COLOR)
+
+ # Apply highlighting
+ for line in data.splitlines():
+ pos = 0
+ while pos < len(line):
+ match_found = False
+ for pattern, color in BIP38Application.JSON_PATTERNS:
+ match = pattern.match(line, pos)
+ if match:
+ cformat = QTextCharFormat()
+ cformat.setForeground(color)
+ cursor.insertText(match.group(), cformat)
+ pos = match.end()
+ match_found = True
+ break
+ if not match_found:
+ cursor.insertText(line[pos], default_format)
+ pos += 1
+ cursor.insertText(end, default_format)
+
+ self.ui.outputQTextEdit.setTextCursor(cursor)
+ self.ui.outputQTextEdit.ensureCursorVisible()
+
+ def logerr(self, err: str , end: str="\n") -> None:
+ cformat = QTextCharFormat()
+ cformat.setForeground(BIP38Application.ERROR_COLOR)
+
+ default_format = QTextCharFormat()
+ default_format.setForeground(BIP38Application.TEXT_COLOR)
+
+ cursor = self.ui.outputQTextEdit.textCursor()
+ cursor.movePosition(QTextCursor.End)
+ cursor.insertText("ERROR", cformat)
+ cursor.insertText(f": {err}{end}", default_format)
+
+ self.ui.outputQTextEdit.setTextCursor(cursor)
+ self.ui.outputQTextEdit.ensureCursorVisible()
+
+ ## validation related functions
+
+ class ValidationError(Exception):
+ def __init__(self, message):
+ super().__init__(message)
+
+ def format_required(self, invalid_inputs):
+ if len(invalid_inputs) == 1:
+ return f"{invalid_inputs[0]} is required"
+ if len(invalid_inputs) == 2:
+ return f"{invalid_inputs[0]} and {invalid_inputs[1]} are required"
+
+ return f"{', '.join(invalid_inputs[:-1])}, and {invalid_inputs[-1]} are required"
+
+ def validate_and_get(self, *args):
+ self.clean_all_required() # forget old validations
+
+ all_data = ()
+ invalid_inputs = []
+
+ for input_key in args:
+ input_data = self.inputs[input_key]
+ text = input_data["input"].text()
+
+ all_data += (text,)
+ if not self.validate_input(input_key, text, validate_optional=True):
+ invalid_inputs.append(input_key)
+
+ if invalid_inputs:
+ raise BIP38Application.ValidationError(self.format_required(invalid_inputs))
+
+ return all_data
+
+ def validate_input(self, input_key, text, validate_optional=False):
+ input_data = self.inputs[input_key]
+
+ qt_input = input_data["input"]
+ optional = input_data["optional"] if validate_optional else True # Handle all fields as optional if validate_optional is true.
+ min_length = input_data["min_length"]
+
+ is_valid = len(text) >= min_length or (optional and len(text) == 0)
+ self.set_required(qt_input, not is_valid)
+
+ return is_valid
+
+ def set_required(self, widget, value=True):
+ widget.setProperty("required", value)
+ widget.style().unpolish(widget)
+ widget.style().polish(widget)
+ widget.update()
+
+ def clean_all_required(self):
+ for key, item in self.inputs.items():
+ self.set_required(item["input"], False)
+
+ def validate_int(self, line_edit):
+ text = line_edit.text()
+ if not text.isdigit():
+ line_edit.setText(''.join(filter(str.isdigit, text)))
+
+ def enforce_int_range(self, line_edit, min_value, max_value):
+ text = line_edit.text()
+ validator = line_edit.validator()
+
+ if text.isdigit():
+ value = int(text)
+ value = max(min_value, min(value, max_value)) # clamp
+ line_edit.setText(str(value))
+
+ def regex_validator(self, regex):
+ return QRegularExpressionValidator(QRegularExpression(regex))
diff --git a/desktop/ui/__init__.py b/desktop/ui/__init__.py
new file mode 100644
index 0000000..b6e3e62
--- /dev/null
+++ b/desktop/ui/__init__.py
@@ -0,0 +1,5 @@
+#!/usr/bin/env python3
+
+# Copyright © 2024, Eyoel Tadesse
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or https://opensource.org/license/mit
\ No newline at end of file
diff --git a/desktop/ui/bip38.ui b/desktop/ui/bip38.ui
new file mode 100644
index 0000000..09dbfbb
--- /dev/null
+++ b/desktop/ui/bip38.ui
@@ -0,0 +1,1832 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 900
+ 491
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 900
+ 0
+
+
+
+
+ 900
+ 16777215
+
+
+
+ MainWindow
+
+
+
+
+ 10
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 280
+
+
+
+
+ 16777215
+ 280
+
+
+
+ QFrame::Shape::StyledPanel
+
+
+ QFrame::Shadow::Raised
+
+
+
+ 7
+
+
+ 15
+
+
+ 15
+
+
+ 15
+
+
+ 5
+
+
-
+
+
+ QFrame::Shape::StyledPanel
+
+
+ QFrame::Shadow::Raised
+
+
+
+ 10
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ 0
+
+
+
+ -
+
+
+
+ 150
+ 16777215
+
+
+
+
+ 5
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 15
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Cryptocurrency
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+ -
+
+
+ (Select)
+
+
+
+
+
+
+ -
+
+
+
+ 115
+ 16777215
+
+
+
+
+ 5
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 15
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Network
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+ -
+
+
+ (Select)
+
+
+
+
+
+
+ -
+
+
+
+ 5
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 15
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Passphrase
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+ 145
+ 16777215
+
+
+
+
+ 5
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 15
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Mode
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+ -
+
+
+ (Select)
+
+
+
+
+
+
+
+
+
+ -
+
+
+ 0
+
+
+
+
+ 7
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ QFrame::Shape::StyledPanel
+
+
+ QFrame::Shadow::Raised
+
+
+
+ 10
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 5
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 15
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Private Key
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+ 145
+ 16777215
+
+
+
+
+ 5
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 15
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ WIF Type
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+ -
+
+
+ (Select)
+
+
+
+
+
+
+ -
+
+
+
+ 115
+ 16777215
+
+
+
+
+ 5
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Qt::LayoutDirection::LeftToRight
+
+
+
+ 5
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Qt::Orientation::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+ PointingHandCursor
+
+
+ Convert
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+ QFrame::Shape::StyledPanel
+
+
+ QFrame::Shadow::Raised
+
+
+
+ 15
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 5
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 15
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Wallet Important Format (WIF)
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+ -
+
+
+ QFrame::Shape::StyledPanel
+
+
+ QFrame::Shadow::Raised
+
+
+
+ 10
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ PointingHandCursor
+
+
+ Encrypt
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 7
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ QFrame::Shape::StyledPanel
+
+
+ QFrame::Shadow::Raised
+
+
+
+ 10
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 5
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 15
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Owner Salt
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+ -
+
+
+ QFrame::Shape::StyledPanel
+
+
+ QFrame::Shadow::Raised
+
+
+
+ 10
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ PointingHandCursor
+
+
+ Generate
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ 5
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 15
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Seed
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+ -
+
+
+ QFrame::Shape::StyledPanel
+
+
+ QFrame::Shadow::Raised
+
+
+
+ 10
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ PointingHandCursor
+
+
+ Generate
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ 100
+ 16777215
+
+
+
+
+ 5
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 15
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Lot
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+ -
+
+
+ QFrame::Shape::StyledPanel
+
+
+ QFrame::Shadow::Raised
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ 100000
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ 100
+ 16777215
+
+
+
+
+ 5
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 15
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Sequence
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+ -
+
+
+ QFrame::Shape::StyledPanel
+
+
+ QFrame::Shadow::Raised
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+ QFrame::Shape::StyledPanel
+
+
+ QFrame::Shadow::Raised
+
+
+
+ 10
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 5
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 15
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Intermediate Passphrase
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+ -
+
+
+ QFrame::Shape::StyledPanel
+
+
+ QFrame::Shadow::Raised
+
+
+
+ 10
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ PointingHandCursor
+
+
+ Generate
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ 5
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 15
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Confirm Code
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+ -
+
+
+ QFrame::Shape::StyledPanel
+
+
+ QFrame::Shadow::Raised
+
+
+
+ 10
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ PointingHandCursor
+
+
+ Verify
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ 10
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ QFrame::Shape::StyledPanel
+
+
+ QFrame::Shadow::Raised
+
+
+
+ 5
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 15
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Encrypted WIF
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+ 145
+ 16777215
+
+
+
+ QFrame::Shape::StyledPanel
+
+
+ QFrame::Shadow::Raised
+
+
+
+ 5
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 15
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ WIF Type
+
+
+
+ -
+
+
+ Qt::Orientation::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+ -
+
+
+
+ 145
+ 0
+
+
+
+ (Select)
+
+
+
+
+
+
+ -
+
+
+ QFrame::Shape::StyledPanel
+
+
+ QFrame::Shadow::Raised
+
+
+
+ 5
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Qt::Orientation::Vertical
+
+
+
+ 20
+ 49
+
+
+
+
+ -
+
+
+ QFrame::Shape::StyledPanel
+
+
+ QFrame::Shadow::Raised
+
+
+
+ 10
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ PointingHandCursor
+
+
+ Create
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ PointingHandCursor
+
+
+ Decrypt
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 200
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/desktop/ui/css/theme.css b/desktop/ui/css/theme.css
new file mode 100644
index 0000000..20c9d92
--- /dev/null
+++ b/desktop/ui/css/theme.css
@@ -0,0 +1,231 @@
+QMainWindow {
+ background-color: #11111b;
+}
+
+QWidget {
+ outline: none;
+ color: white;
+ font: 10pt "Segoe UI";
+}
+
+QFrame{
+ background-color: transparent;
+ border: none;
+}
+
+QLabel {
+ font: 10pt "HDWallet";
+ letter-spacing: 1px;
+ color: rgb(255, 255, 255);
+ text-transform: uppercase;
+}
+
+
+/* Scroll bar */
+QScrollBar:vertical {
+ border: none;
+ background: rgb(16, 21, 27);
+ width: 8px;
+ border-radius: 4px;
+}
+
+ QScrollBar::handle:vertical {
+ background: #65619c;
+ min-height: 55px;
+ border-radius: 4px;
+ }
+
+ QScrollBar::add-line:vertical {
+ border: none;
+ height: 20px;
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ subcontrol-position: bottom;
+ subcontrol-origin: margin;
+ }
+
+ QScrollBar::sub-line:vertical {
+ border: none;
+ height: 20px;
+ border-top-left-radius: 0px;
+ border-top-right-radius: 0px;
+ subcontrol-position: top;
+ subcontrol-origin: margin;
+ }
+
+ QScrollBar::up-arrow:vertical,
+ QScrollBar::down-arrow:vertical {
+ background: transparent;
+ }
+
+QScrollBar::add-page:vertical,
+QScrollBar::sub-page:vertical{
+ background: rgba(0, 0, 0, 0.3);
+}
+
+
+QScrollBar:horizontal {
+ border: none;
+ background: rgb(16, 21, 27);
+ width: 8px;
+ border-radius: 4px;
+}
+
+/* Inputs */
+QComboBox {
+ background-color: rgba(28, 28, 37, 1);
+ border-radius: 5px;
+ color: rgb(255, 255, 255);
+ padding: 5px 10px;
+ height: 25px;
+}
+
+
+QComboBox:disabled{
+ background-color: rgba(32, 36, 43, 0.5);
+}
+
+QComboBox::drop-down {
+ subcontrol-origin: padding;
+ subcontrol-position: top right;
+ width: 30px;
+ border-left-width: 3px;
+ border-left-style: solid;
+ background-image: url(:/png/images/png/cil-arrow-bottom.png);
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+QComboBox::drop-down:checked {
+ background-image: url(:/png/images/png/cil-arrow-top.png);
+}
+
+QComboBox::drop-down:hover {
+ background-color: transparent;
+}
+
+
+QComboBox QAbstractItemView {
+ color: white;
+ border: none;
+ padding: 10px;
+ background-color: rgba(28, 28, 37, 1);
+ outline: none;
+ border-bottom: 2px solid rgba(0, 0, 0, 0.5);
+}
+
+QComboBox QAbstractItemView::item {
+ padding: 5px;
+ width: fit-content;
+}
+
+QComboBox QAbstractItemView::item:hover {
+ outline: none;
+}
+
+QComboBox QAbstractItemView::item:selected {
+ background-color: #11111b;
+ color: white;
+ selection-background-color: white;
+ outline: none;
+}
+
+QLineEdit {
+ font: 9pt "Segoe UI";
+ letter-spacing: 1px;
+ background-color: rgba(28, 28, 37, 1);
+ border: 1px solid rgba(0, 0, 0, 0.0);
+ border-radius: 5px;
+ color: rgb(255, 255, 255);
+ padding: 4px 7px;
+ height: 25px;
+ selection-color: #DDD;
+ selection-background-color: #65619c;
+}
+
+QLineEdit:hover {
+ background-color: rgba(28, 28, 37, 0.7);
+}
+
+QLineEdit::disabled {
+ background-color: rgba(25, 30, 36, 0.5);
+}
+
+QLineEdit[required="true"] {
+ border: 1px solid rgba(255, 96, 96, 0.5);
+}
+
+QTextEdit {
+ background-color: black;
+ border-color: rgb(25, 30, 36);
+ color: rgb(255, 255, 255);
+ padding: 15px 5px;
+ padding-top: 10px;
+ selection-color: #DDD;
+ selection-background-color: #65619c;
+}
+
+QPushButton {
+ font: 10pt "HDWallet";
+ letter-spacing: 1px;
+ font-weight: lighter;
+ letter-spacing: 1px;
+ background-color: rgba(28, 28, 37, 1);
+ border-radius: 3px;
+ color: rgb(255, 255, 255);
+ padding: 5px 10px;
+ height: 25px;
+}
+
+QPushButton:hover{
+ background-color: rgba(28, 28, 37, 0.7);
+}
+
+/* QMENU */
+
+QMenu {
+ background-color: #11111b;
+ color: white;
+ border: 1px solid rgba(128, 128, 128, 0.25);
+ padding: 5px;
+ border-radius: 4px;
+}
+
+QMenu::item {
+ background-color: transparent;
+ padding: 5px 15px;
+ color: white;
+ qproperty-icon: none;
+}
+
+QMenu::item:selected {
+ background: rgb(25, 30, 36);
+ color: #65619c;
+ border-radius: 3px;
+}
+
+QMenu::item:hover {
+ background-color: rgb(25, 30, 36);
+ color: #65619c;
+ border-radius: 3px;
+}
+
+QMenu::item:disabled {
+ background-color: transparent;
+ color: rgba(255, 255, 255, 0.5);
+}
+
+QMenu::item:disabled:selected {
+ color: rgba(255, 255, 255, 0.5);
+ background-color: transparent;
+}
+
+QMenu::separator {
+ height: 1px;
+ background-color: rgba(128, 128, 128, 0.25);
+ margin: 5px 0;
+}
+
+QMenu:disabled {
+ color: rgba(255, 255, 255, 0.5);
+}
diff --git a/desktop/ui/font/HDWallet.ttf b/desktop/ui/font/HDWallet.ttf
new file mode 100644
index 0000000..ea686d7
Binary files /dev/null and b/desktop/ui/font/HDWallet.ttf differ
diff --git a/desktop/ui/images/icon/icon.ico b/desktop/ui/images/icon/icon.ico
new file mode 100644
index 0000000..6470bed
Binary files /dev/null and b/desktop/ui/images/icon/icon.ico differ
diff --git a/desktop/ui/images/svg/full-logo.svg b/desktop/ui/images/svg/full-logo.svg
new file mode 100644
index 0000000..fed49cb
--- /dev/null
+++ b/desktop/ui/images/svg/full-logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/desktop/ui/images/svg/logo.svg b/desktop/ui/images/svg/logo.svg
new file mode 100644
index 0000000..7e9c7a5
--- /dev/null
+++ b/desktop/ui/images/svg/logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/desktop/ui/resources.qrc b/desktop/ui/resources.qrc
new file mode 100644
index 0000000..a733b9f
--- /dev/null
+++ b/desktop/ui/resources.qrc
@@ -0,0 +1,6 @@
+
+
+ images/png/cil-arrow-top.png
+ images/png/cil-arrow-bottom.png
+
+
diff --git a/desktop/ui/resources_rc.py b/desktop/ui/resources_rc.py
new file mode 100644
index 0000000..374b2d9
--- /dev/null
+++ b/desktop/ui/resources_rc.py
@@ -0,0 +1,405 @@
+# Resource object code (Python 3)
+# Created by: object code
+# Created by: The Resource Compiler for Qt version 6.7.2
+# WARNING! All changes made in this file will be lost!
+
+from PySide6 import QtCore
+
+qt_resource_data = b"\
+\x00\x00\x06\xed\
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xffa\
+\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\
+\x01\x00\x9a\x9c\x18\x00\x00\x05\xf1iTXtXML\
+:com.adobe.xmp\x00\x00\
+\x00\x00\x00 \
+ \
+ <\
+/xmpMM:History> \
+ \
+x:xmpmeta> \x8d\x0b\
+\x9e5\x00\x00\x00\xa2IDAT8\x11c\xf8\xff\xff\
+?\x03%\x98d\x0d\xcf\x9e=c\x04a\xb2\x0c\x00j\
+d\x22\xdb\x05@\xcd\xccP\xda\x04\x88\x8bH2\x00I\
+\xb3&\x10?\x05\xe2\x0a\x98w0\xfcG\x84\xe6bd\
+\xb5\x18\x1a\x91\xfd\x89\xa6\xf9\x09\xcc\xe90qd\x03\xa4\
+\x80x'\x10\xebB\xf9,Xl\xc6\xd0\x8c\x12\x06@\
+\x892 ~\x0e\xc4\x86Hb\xe8\xcef\xc6\x1b\x0b@\
+\x05i@\xfc\x16\x88-\x81X\xe5\x19\x04\xe0\xd4\x8c\xee\
+\x02&(\x1d\x01\xb5\x15\xa7\xb3\xf1\xb9\x00fH\x00\x10\
+\xe7\xe2J \
+ \
+ <\
+/xmpMM:History> \
+ \
+x:xmpmeta> \xacr\
+\xe7\xb6\x00\x00\x00\xd2IDAT8\x8d\xc5\xd31J\
+\x041\x14\x87\xf1_f#.\x82\xde\xc4R\x0f\xb0\xae\
+'\xf0\x06\xb66\xdeD\xef`c\xad\x9d`+X\x8f\
+\xe0^\xc0\xca\xc6~\xdd\x89\x85\x09\x86a\x06e\xa70\
+M\xf8\x93|_\xde{\x90\x90R2e5\x93h\xc4\
+\xb6m\x1btS*\xe80\xdb\x82\x0dE\x00\x9b-\x04\
+\xa9\x16\xec\xe3\xf8\x8f\xe0A}\xb7\x08\xe6\xb8\xc1\xe5/\
+p\xc0-.J\x8eY\xf2\x8e3<\xe4\x83\xeb\x0a\xda\
+\xc1:\xc3\xf79\x9f\x976\xa2\x9f!\xbe\xe0\x04\x8f=\
+\xc9:\xefw\xd8\xc5iUM\x8a9l\x10\xf1ZI\
+\x02\xae\xaa\x97\xe7XV\xadw2T\xd6\xe7\x80\xa4\xc1\
+\x11\xf6\xb0\xe8\xc3}A_\xb2\xc0\x13\x9e\xc7\xe0!A\
+\x91\xcc\xb0\xc2!>\xc6\xe01\x01\xdf3i\xf0\x96s\
+\x18\x82!\xfc\xfbo\xfc\x02d\x060\xdc\x02*\xb0\x83\
+\x00\x00\x00\x00IEND\xaeB`\x82\
+\x00\x00\x07 \
+\x89\
+PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\
+\x00\x00\x10\x00\x00\x00\x10\x08\x06\x00\x00\x00\x1f\xf3\xffa\
+\x00\x00\x00\x09pHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\
+\x01\x00\x9a\x9c\x18\x00\x00\x05\xf1iTXtXML\
+:com.adobe.xmp\x00\x00\
+\x00\x00\x00 \
+ \
+ <\
+/xmpMM:History> \
+ \
+x:xmpmeta> \xaa~\
+\x0dL\x00\x00\x00\xd5IDAT8\x8d\xc5\xd3=N\
+\x031\x14\x04\xe0\xcf\xbbKEM\xc5\x05@\xe2(@\
+\x0d\x05\xc7\xa1\x01!8\x01'\x80\x13\xf0s\x80\xb4{\
+\x12@\x22\x89\x021\x05o\x91\x15\x9c\x88(\x05\xd3x\
+<~3z~\x96S\xce\xd9&h6r\xa3\xeb\xfb\
+~U\xf8
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or https://opensource.org/license/mit
+
+import os
+from PySide6.QtWidgets import QLayout
+from PySide6.QtSvgWidgets import QSvgWidget
+from PySide6.QtCore import QSize
+
+def clear_layout(layout: QLayout, delete: bool = True) -> None:
+ """
+ Clear all widgets from a layout.
+
+ :param layout: The layout to clear.
+ :param delete: Whether to delete the widgets after removing them from the layout.
+ """
+ if layout is not None:
+ while layout.count():
+ item = layout.takeAt(0)
+ widget = item.widget()
+ if widget is not None and delete:
+ widget.deleteLater()
+ else:
+ clear_layout(item.layout())
+
+
+def resolve_path(path: str) -> str:
+ """
+ Resolve the absolute path of a given relative path.
+
+ :param path: The relative path to resolve.
+ :type path: str
+ :return: The absolute path of the given relative path.
+ :rtype: str
+ """
+ return os.path.abspath(os.path.join(os.path.dirname(__file__), "../", path))
+
+
+def put_svg(layout: QLayout, path: str, width: int, height: int) -> QSvgWidget:
+ """
+ Add an SVG widget to a layout.
+
+ :param layout: The layout to add the SVG widget to.
+ :param path: The path to the SVG file.
+ :param width: The width of the SVG widget.
+ :param height: The height of the SVG widget.
+ :return: The SVG widget.
+ """
+ clear_layout(layout)
+ svg = QSvgWidget(path)
+ svg.setMinimumSize(QSize(width, height))
+ svg.setMaximumSize(QSize(width, height))
+ svg.setStyleSheet("background: transparent")
+ layout.addWidget(svg)
+ return svg
diff --git a/docs/index.rst b/docs/index.rst
index b7d66be..280ee99 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -455,7 +455,7 @@ We welcome pull requests. To get started, just fork this `github repository
\ No newline at end of file
diff --git a/docs/static/svg/bip38.svg b/docs/static/svg/bip38.svg
new file mode 100644
index 0000000..7e9c7a5
--- /dev/null
+++ b/docs/static/svg/bip38.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/launch.py b/launch.py
new file mode 100644
index 0000000..6b3a25e
--- /dev/null
+++ b/launch.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+
+# Copyright © 2024, Eyoel Tadesse
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or https://opensource.org/license/mit
+
+from PySide6.QtWidgets import QApplication
+from PySide6.QtGui import QPalette, QColor
+from PySide6.QtCore import Qt
+
+import sys
+
+from desktop.main import BIP38Application
+
+
+def main() -> None:
+ qapp: QApplication = QApplication(sys.argv)
+
+ palette = QPalette()
+ palette.setColor(QPalette.Active, QPalette.Text, QColor(255, 255, 255))
+ palette.setColor(QPalette.Disabled, QPalette.Text, QColor(170, 170, 170))
+ qapp.setPalette(palette)
+ qapp.setStyle("Windows")
+
+ main_application: BIP38Application = BIP38Application()
+ main_application.app.show()
+ sys.exit(qapp.exec())
+
+
+if __name__ == '__main__':
+ main()
diff --git a/requirements/desktop.txt b/requirements/desktop.txt
new file mode 100644
index 0000000..01a3fdc
--- /dev/null
+++ b/requirements/desktop.txt
@@ -0,0 +1,2 @@
+PySide6==6.7.2
+cx-Freeze==7.2.0
\ No newline at end of file
diff --git a/setup.py b/setup.py
index f1875fd..d83a261 100644
--- a/setup.py
+++ b/setup.py
@@ -50,7 +50,8 @@ def get_requirements(name: str) -> List[str]:
include_package_data=True,
extras_require=dict(
docs=get_requirements(name="requirements/docs"),
- tests=get_requirements(name="requirements/tests")
+ tests=get_requirements(name="requirements/tests"),
+ desktop=get_requirements(name="requirements/desktop")
),
classifiers=[
"Development Status :: 5 - Production/Stable",