yaml config translations / language & direction change & reload load/save wallets from encrypted files load/save wallets/load last wallet on sartup
1692 lines
72 KiB
Python
Executable File
1692 lines
72 KiB
Python
Executable File
#!.venv/bin/python
|
|
"""Khadhroony SRLPv4 main script"""
|
|
|
|
import argparse
|
|
import base64
|
|
import gettext
|
|
import json
|
|
import locale
|
|
import os
|
|
import platform
|
|
import portalocker
|
|
import random
|
|
import re
|
|
import rc_doc
|
|
import rc_icons
|
|
import rc_images
|
|
import sys
|
|
import yaml
|
|
|
|
from cryptography.fernet import Fernet, InvalidToken
|
|
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
from cryptography.hazmat.primitives import hashes
|
|
from cryptography.hazmat.backends import default_backend
|
|
from datetime import datetime, timezone
|
|
from loguru import logger
|
|
from PySide6 import __version__ as pyside6version
|
|
from PySide6.QtCore import (
|
|
QAbstractTableModel,
|
|
QEasingCurve,
|
|
QEvent,
|
|
QMargins,
|
|
QModelIndex,
|
|
QObject,
|
|
QPropertyAnimation,
|
|
Qt,
|
|
QTimer,
|
|
Signal
|
|
)
|
|
from PySide6.QtGui import (
|
|
QAction,
|
|
QColor,
|
|
QCursor,
|
|
QIcon,
|
|
QKeyEvent,
|
|
QLinearGradient,
|
|
QPainter,
|
|
QPixmap,
|
|
QFont
|
|
)
|
|
from PySide6.QtPdf import QPdfDocument
|
|
from PySide6.QtPdfWidgets import QPdfView
|
|
from PySide6.QtWidgets import (
|
|
QApplication,
|
|
QDialog,
|
|
QDialogButtonBox,
|
|
QFileDialog,
|
|
QFormLayout,
|
|
QGraphicsOpacityEffect,
|
|
QGridLayout,
|
|
QLabel,
|
|
QLineEdit,
|
|
QMainWindow,
|
|
QMenu,
|
|
QMessageBox,
|
|
QSplashScreen,
|
|
QPushButton,
|
|
QSystemTrayIcon,
|
|
QTableView,
|
|
QTabWidget,
|
|
QToolBar,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
QAbstractItemView,
|
|
QHeaderView
|
|
)
|
|
from solders.keypair import Keypair
|
|
from typing import Any, IO
|
|
|
|
APP_NAME: str = "KhadhroonySRLPv4"
|
|
"""Application Name"""
|
|
APP_ABOUT_NAME: str = "Khadhroony Sol RLPv4 Trading App"
|
|
"""Application Full Name"""
|
|
APP_FULL_NAME: str = "Khadhroony Solana Raydium Liquidity Pool v4 Trading App"
|
|
"""Application Full Name"""
|
|
APP_DESC: str = "Khadhroony Solana Raydium Liquidity Pool v4 Trading Application"
|
|
"""Application Version"""
|
|
APP_VERSION_INFO = ('0', '0', '2')
|
|
APP_VERSION = '.'.join(APP_VERSION_INFO)
|
|
"""Application Name + Version"""
|
|
APP_NAME_VERSION = "Khadhroony Solana Raydium Liquidity Pool v4 Trading Application @ " + APP_VERSION
|
|
"""Application Langs"""
|
|
APP_LANGS = ["en", "fr", "ar"]
|
|
APP_RTL_LANGS = [
|
|
"ae", # avestique
|
|
"aeb", # arabe tunisien
|
|
"aec", # arabe saïdi
|
|
"ar", # arabe
|
|
"arb", # arabe standard moderne
|
|
"arc", # araméen
|
|
"arq", # arabe algérien
|
|
"ary", # arabe marocain
|
|
"arz", # arabe égyptien
|
|
"ayl", # arabe libyen
|
|
"ckb", # sorani
|
|
"dv", # divéhi (maldivien)
|
|
"fa", # persan
|
|
"glk", # guilaki
|
|
"he", # hébreu
|
|
"khw", # khowar
|
|
"mzn", # mazandarani
|
|
"ota", # turc ottoman
|
|
"otk", # vieux turc
|
|
"pnb", # pendjabi occidental
|
|
"ps", # pachto
|
|
"syr", # syriaque
|
|
"tmr", # judéo-araméen babylonien
|
|
"ug", # ouïghour
|
|
"ur" # ourdou
|
|
]
|
|
"""Application Salt for wallet encrypting"""
|
|
APP_SALT = APP_NAME + " @ " + APP_VERSION_INFO[0] + "." + APP_VERSION_INFO[1]
|
|
|
|
YAML_CONFIG_FILE = "KhadhroonySRLPv4.yaml"
|
|
|
|
DEFAULT_YAML_CONFIG = {
|
|
"lastFile": "",
|
|
"lastFiles": [],
|
|
"defaultLang": "en",
|
|
"lastWidth": 1024,
|
|
"lastHeight": 768,
|
|
"lastMaximized": True
|
|
}
|
|
|
|
|
|
WALLET_FILE_EXT = "kew"
|
|
WALLET_FILE_DESC = "Khadhroony Encrypted Wallet"
|
|
WALLETS_FOLDER = "./wallets"
|
|
|
|
LOCALES_DIR = os.path.join(os.path.dirname(__file__), "locale")
|
|
_ = gettext.gettext
|
|
is_rtl = False
|
|
|
|
|
|
def load_yaml_app_config():
|
|
"""Load the configuration file or create a default one."""
|
|
if not os.path.exists(YAML_CONFIG_FILE):
|
|
logger.debug(f"{YAML_CONFIG_FILE} not found, creating new one")
|
|
with open(YAML_CONFIG_FILE, "w") as f:
|
|
yaml.dump(DEFAULT_YAML_CONFIG, f)
|
|
return DEFAULT_YAML_CONFIG
|
|
with open(YAML_CONFIG_FILE, "r") as f:
|
|
data = yaml.safe_load(f)
|
|
logger.debug(f"{YAML_CONFIG_FILE} safe loaded")
|
|
# Validation et complétion des données
|
|
for key, val in DEFAULT_YAML_CONFIG.items():
|
|
if key not in data:
|
|
logger.debug(f"{key} not found")
|
|
data[key] = val
|
|
elif not isinstance(data[key], type(val)):
|
|
logger.debug(f"{key} wrong type : wanted {type(val)}, found {type(data[key])}")
|
|
data[key] = val
|
|
return data
|
|
|
|
|
|
def save_yaml_app_config(cfg):
|
|
"""Save the configuration to the file."""
|
|
with open(YAML_CONFIG_FILE, "w") as f:
|
|
yaml.dump(cfg, f)
|
|
|
|
|
|
def create_wallets_folder():
|
|
"""Vérifie si le répertoire wallets existe et le créé dans le cas contraire."""
|
|
if not os.path.exists(WALLETS_FOLDER):
|
|
os.makedirs(WALLETS_FOLDER)
|
|
|
|
|
|
def derive_key(password: str, salt: bytes) -> bytes:
|
|
"""
|
|
Derive a symmetric key from the given password and salt.
|
|
:param password: The password to derive the key from.
|
|
:param salt: A random salt for the key derivation.
|
|
:return: The derived key.
|
|
"""
|
|
kdf = PBKDF2HMAC(
|
|
algorithm = hashes.SHA256(),
|
|
length = 32,
|
|
salt = salt,
|
|
iterations = 100_000,
|
|
backend = default_backend(),
|
|
)
|
|
return base64.urlsafe_b64encode(kdf.derive(password.encode()))
|
|
|
|
|
|
def encrypt_string(password: str, salt: bytes, plaintext: str) -> str:
|
|
"""
|
|
Encrypts a string using a password.
|
|
:param password: The password to derive the encryption key.
|
|
:param salt: A salt for the key derivation.
|
|
:param plaintext: The string to encrypt.
|
|
:return: A tuple containing the salt and the encrypted string.
|
|
"""
|
|
key = derive_key(password, salt) # Derive the encryption key
|
|
fernet = Fernet(key)
|
|
encrypted_text = fernet.encrypt(plaintext.encode())
|
|
return encrypted_text.decode()
|
|
|
|
|
|
def center_on_screen(widget: QWidget):
|
|
"""Centre la fenêtre sur le moniteur actif (celui où le curseur est présent)."""
|
|
screen = QApplication.screenAt(QCursor().pos()) # Obtenir le moniteur actif
|
|
if not screen:
|
|
screen = QApplication.primaryScreen() # Par défaut, utiliser l'écran principal
|
|
|
|
screen_geometry = screen.availableGeometry() # Dimensions de l'écran
|
|
widget_geometry = widget.frameGeometry()
|
|
# Calcul des coordonnées pour centrer la fenêtre
|
|
x = screen_geometry.x() + (screen_geometry.width() - widget_geometry.width()) // 2
|
|
y = screen_geometry.y() + (screen_geometry.height() - widget_geometry.height()) // 2
|
|
widget.move(x, y)
|
|
|
|
|
|
def decrypt_string(password: str, salt: bytes, encrypted_text: bytes | str) -> str:
|
|
"""
|
|
Decrypts an encrypted string using the given password and salt.
|
|
:param password: The password to derive the decryption key.
|
|
:param salt: The salt used during encryption.
|
|
:param encrypted_text: The encrypted string to decrypt.
|
|
:return: The decrypted string.
|
|
"""
|
|
enc_text = encrypted_text
|
|
if isinstance(encrypted_text, str):
|
|
enc_text = encrypted_text.encode()
|
|
key = derive_key(password, salt) # Derive the decryption key
|
|
fernet = Fernet(key)
|
|
return fernet.decrypt(enc_text).decode()
|
|
|
|
|
|
def get_system_language() -> str:
|
|
"""Get the system language."""
|
|
global is_rtl
|
|
# sys_locale = locale.getdefaultlocale()[0]
|
|
sys_locale = locale.getlocale()[0]
|
|
default_lang = 'en'
|
|
if sys_locale:
|
|
lang_split = sys_locale.split("_")[0] # Extract language code
|
|
if lang_split in APP_LANGS:
|
|
if lang_split in APP_RTL_LANGS:
|
|
is_rtl = True
|
|
else:
|
|
is_rtl = False
|
|
return lang_split
|
|
is_rtl = False
|
|
return default_lang
|
|
|
|
|
|
def get_lang_or_fallback(lang: str) -> str:
|
|
global is_rtl
|
|
if lang in APP_LANGS:
|
|
if lang in APP_RTL_LANGS:
|
|
is_rtl = True
|
|
else:
|
|
is_rtl = False
|
|
return lang
|
|
return get_system_language()
|
|
|
|
|
|
def set_app_lang_code(lang_code):
|
|
"""Set the application language."""
|
|
global _
|
|
global is_rtl
|
|
try:
|
|
lang = gettext.translation("app", localedir = LOCALES_DIR, languages = [lang_code])
|
|
lang.install()
|
|
_ = lang.gettext
|
|
if lang_code in APP_RTL_LANGS:
|
|
is_rtl = True
|
|
else:
|
|
is_rtl = False
|
|
except FileNotFoundError:
|
|
# Fallback to default language (English)
|
|
gettext.install("app", localedir = LOCALES_DIR, names = ["en"])
|
|
_ = gettext.gettext
|
|
is_rtl = False
|
|
|
|
|
|
def get_qt_dir() -> Qt.LayoutDirection:
|
|
global is_rtl
|
|
return Qt.LayoutDirection.RightToLeft if is_rtl else Qt.LayoutDirection.LeftToRight
|
|
|
|
|
|
def get_qt_align_center() -> Qt.AlignmentFlag:
|
|
return Qt.AlignmentFlag.AlignCenter
|
|
|
|
|
|
def get_qt_align_vchl(rtl:bool = False) -> Qt.AlignmentFlag:
|
|
if rtl:
|
|
return Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignRight
|
|
else:
|
|
return Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft
|
|
|
|
|
|
def get_qt_align_vchr(rtl:bool = False) -> Qt.AlignmentFlag:
|
|
if rtl:
|
|
return Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft
|
|
else:
|
|
return Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignRight
|
|
|
|
|
|
def get_qt_align_hcenter() -> Qt.AlignmentFlag:
|
|
return Qt.AlignmentFlag.AlignHCenter
|
|
|
|
|
|
def get_qt_align_hl(rtl:bool = False) -> Qt.AlignmentFlag:
|
|
if rtl:
|
|
return Qt.AlignmentFlag.AlignRight
|
|
else:
|
|
return Qt.AlignmentFlag.AlignLeft
|
|
|
|
|
|
def get_qt_align_hr(rtl:bool = False) -> Qt.AlignmentFlag:
|
|
if rtl:
|
|
return Qt.AlignmentFlag.AlignLeft
|
|
else:
|
|
return Qt.AlignmentFlag.AlignRight
|
|
|
|
class LockedFile:
|
|
"""
|
|
Une classe pour gérer les fichiers avec verrouillage exclusif.
|
|
Utilise portalocker pour éviter les accès simultanés.
|
|
"""
|
|
|
|
def __init__(self, file_path, mode = "r"):
|
|
"""
|
|
Initialise un Verrou.
|
|
|
|
:param file_path: Chemin vers le fichier à verrouiller.
|
|
:param mode: Mode d'ouverture du fichier ('r', 'w', 'a', etc.).
|
|
"""
|
|
self._file = None
|
|
self._filePath = file_path
|
|
self._mode = mode
|
|
|
|
def __enter__(self) -> IO[Any]:
|
|
"""
|
|
Ouvre et verrouille le fichier.
|
|
"""
|
|
self._file = open(file = self._filePath, mode = self._mode)
|
|
# Application d'un verrou exclusif
|
|
portalocker.lock(self._file, portalocker.LOCK_EX)
|
|
return self._file
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
"""
|
|
Libère le verrou et ferme le fichier.
|
|
"""
|
|
try:
|
|
portalocker.unlock(self._file)
|
|
finally:
|
|
self._file.close()
|
|
|
|
|
|
class SplashScreen(QSplashScreen):
|
|
"""SplashScreen class."""
|
|
|
|
def __init__(self, fade_in_duration = 1500, display_duration = 2000, fade_out_duration = 1500):
|
|
super().__init__(QPixmap(":imgSplash"))
|
|
# logger.info(f"SplashScreen {Consts.APP_FULL_NAME} ({Consts.APP_VERSION})")
|
|
self.setWindowFlag(Qt.WindowType.FramelessWindowHint) # Supprimer les boutons de fermeture
|
|
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
|
# Appliquer l'effet d'opacité pour gérer les transitions
|
|
self._opacityEffect = QGraphicsOpacityEffect(self)
|
|
self.setGraphicsEffect(self._opacityEffect)
|
|
# Animation Fade-in
|
|
self._fadeIn = QPropertyAnimation(self._opacityEffect, b"opacity")
|
|
self._fadeIn.setDuration(fade_in_duration)
|
|
self._fadeIn.setStartValue(0)
|
|
self._fadeIn.setEndValue(1)
|
|
self._fadeIn.setEasingCurve(QEasingCurve.Type.InOutQuad)
|
|
# Animation Fade-out
|
|
self.fadeOut = QPropertyAnimation(self._opacityEffect, b"opacity")
|
|
self.fadeOut.setDuration(fade_out_duration)
|
|
self.fadeOut.setStartValue(1)
|
|
self.fadeOut.setEndValue(0)
|
|
self.fadeOut.setEasingCurve(QEasingCurve.Type.InOutQuad)
|
|
# Planification de l'animation fade-out après la durée
|
|
QTimer.singleShot(display_duration, self.start_fade_out)
|
|
# Lancer l'animation fade-in
|
|
self._fadeIn.start()
|
|
|
|
def start_fade_out(self):
|
|
"""Lancer l'animation fade-out et fermer le splash screen à la fin."""
|
|
self.fadeOut.start()
|
|
self.fadeOut.finished.connect(self.close)
|
|
|
|
def showEvent(self, event) -> None:
|
|
super().showEvent(event)
|
|
center_on_screen(self)
|
|
|
|
|
|
class AboutWindow(QWidget):
|
|
|
|
def __init__(self, bg_txt:str, parent = None):
|
|
super().__init__(parent)
|
|
self.setWindowTitle(_("About %(app)s") % {'app': APP_NAME_VERSION})
|
|
self.setWindowIcon(QIcon(":imgIcon"))
|
|
self.setFixedSize(900, 600)
|
|
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
|
self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint)
|
|
self._bg_Qpix = QPixmap(":imgAbout")
|
|
# Texte centré placé avant le bouton de fermeture pour ne pas empêcher le click de celui-là
|
|
self._about_Qlbl = QLabel(self)
|
|
self._about_Qlbl.setText(bg_txt)
|
|
self._about_Qlbl.setAlignment(Qt.AlignmentFlag.AlignCenter) # Centrer le texte
|
|
self._about_Qlbl.setStyleSheet("font-size: 20px; color: white;") # Style du texte
|
|
self._about_Qlbl.setWordWrap(True) # Activer le retour à la ligne
|
|
self._about_Qlbl.setGeometry(0, 0, self.width(), self.height()) # Occuper toute la fenêtre
|
|
# Bouton de fermeture de la fenetre
|
|
self._exit_Qpbtn = QPushButton(self)
|
|
self._exit_Qpbtn.setIcon(QIcon(":icoQuit"))
|
|
self._exit_Qpbtn.setFixedSize(24, 24)
|
|
self._exit_Qpbtn.setGeometry(self.width() - 44, 20, 24, 24) # Position et taille
|
|
self._exit_Qpbtn.clicked.connect(self.close)
|
|
self._exit_Qpbtn.move(self.width() - self._exit_Qpbtn.width() - 20, 20)
|
|
|
|
# self.outsideClickFilter = OutsideCloseClickFilter(self)
|
|
# QApplication.instance().installEventFilter(self.outsideClickFilter)
|
|
|
|
def closeEvent(self, event) -> None:
|
|
# QApplication.instance().removeEventFilter(self.outsideClickFilter)
|
|
super().closeEvent(event)
|
|
|
|
def paintEvent(self, event):
|
|
painter = QPainter(self)
|
|
painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
|
|
# Redimensionner l'image pour qu'elle corresponde à la taille de la fenêtre
|
|
scaled_image = self._bg_Qpix.scaled(
|
|
self.size(),
|
|
Qt.AspectRatioMode.IgnoreAspectRatio,
|
|
Qt.TransformationMode.SmoothTransformation,
|
|
)
|
|
painter.drawPixmap(0, 0, scaled_image)
|
|
painter.setOpacity(0.7)
|
|
# Ajouter un effet de dégradé
|
|
gradient = QLinearGradient(0, 0, 0, self.height())
|
|
gradient.setColorAt(0, QColor(0, 0, 0, 0)) # Transparent en haut
|
|
gradient.setColorAt(1, QColor(0, 0, 0, 128)) # Sombre en bas
|
|
painter.fillRect(self.rect(), gradient)
|
|
|
|
def event(self, event):
|
|
"""Surveiller les événements de la fenêtre."""
|
|
if event.type() == QEvent.Type.WindowDeactivate:
|
|
self.close()
|
|
return super().event(event)
|
|
|
|
def keyPressEvent(self, event: QKeyEvent):
|
|
"""Gérer les raccourcis clavier pour fermer la fenêtre."""
|
|
if event.key() in {Qt.Key.Key_Escape, Qt.Key.Key_Space, Qt.Key.Key_Enter, Qt.Key.Key_Return}:
|
|
print("Fenêtre fermée avec une touche simple.")
|
|
self.close()
|
|
elif event.key() == Qt.Key.Key_F4 and event.modifiers() & Qt.KeyboardModifier.ControlModifier:
|
|
print("Fenêtre fermée avec Ctrl+F4.")
|
|
self.close()
|
|
else:
|
|
# Si une autre touche est pressée, laisser le comportement par défaut
|
|
super().keyPressEvent(event)
|
|
|
|
|
|
class HelpWindow(QWidget):
|
|
|
|
def showEvent(self, event) -> None:
|
|
super().showEvent(event)
|
|
center_on_screen(self)
|
|
|
|
def __init__(self, pdf_doc:str, parent = None):
|
|
super().__init__(parent)
|
|
self.setWindowTitle(_("Help - %(app)s") % {'app': APP_NAME_VERSION})
|
|
# self.setWindowIcon(QIcon(":/res/images/favicon.ico"))
|
|
self.setWindowIcon(QIcon(":imgIcon"))
|
|
self.setFixedSize(900, 600)
|
|
self.setWindowModality(Qt.WindowModality.ApplicationModal)
|
|
self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.MSWindowsFixedSizeDialogHint)
|
|
# Créer le document PDF et le visualiseur PDF
|
|
self._pdfDoc = QPdfDocument(self)
|
|
self._pdfView = QPdfView()
|
|
self._pdfView.setDocument(self._pdfDoc)
|
|
# Configurer le widget central pour afficher le PDF
|
|
self._main_Qvlo = QVBoxLayout()
|
|
self.setLayout(self._main_Qvlo)
|
|
self._main_Qvlo.setSpacing(0)
|
|
self._main_Qvlo.setContentsMargins(QMargins(0, 0, 0, 0))
|
|
self._main_Qvlo.addWidget(self._pdfView)
|
|
# self._pdfDoc.load(":/res/help.pdf")
|
|
self._pdfDoc.load(pdf_doc)
|
|
if self._pdfDoc.status() != QPdfDocument.Status.Ready:
|
|
self.close()
|
|
else:
|
|
# Afficher la première page du PDF
|
|
self._pdfView.setPageMode(QPdfView.PageMode.MultiPage)
|
|
self._pdfView.setZoomMode(QPdfView.ZoomMode.FitToWidth)
|
|
self._pdfView.setPageSpacing(0)
|
|
self._pdfView.setDocumentMargins(QMargins(0, 0, 0, 0))
|
|
|
|
|
|
class LogEmitter(QObject):
|
|
"""
|
|
Objet émettant un signal pour envoyer les logs vers l'interface graphique.
|
|
"""
|
|
newLogSignal = Signal(dict) # Le log est transmis sous forme de dictionnaire
|
|
|
|
|
|
class LogHandler:
|
|
"""
|
|
Gestionnaire personnalisé pour diriger les logs de loguru vers un QTableView.
|
|
"""
|
|
|
|
def __init__(self, emitter: LogEmitter):
|
|
self._logEmitter = emitter
|
|
|
|
def __call__(self, message: str):
|
|
"""
|
|
Appelé par loguru pour chaque message de log.
|
|
"""
|
|
parsed_log = self.parse_log_message(message)
|
|
if parsed_log:
|
|
self._logEmitter.newLogSignal.emit(parsed_log)
|
|
|
|
@staticmethod
|
|
def parse_log_message(message: str):
|
|
"""
|
|
Parse un message de log loguru au format attendu (timestamp, LEVEL, message).
|
|
"""
|
|
parts = message.split(" -|- ", 2) # On attend le format "{time} -|- {level} -|- {message}"
|
|
if len(parts) == 3:
|
|
return {
|
|
"timestamp": parts[0], # Timestamp
|
|
"level": parts[1], # Niveau du log
|
|
"message": parts[2] # Message
|
|
}
|
|
return None
|
|
|
|
|
|
class LogsTableModel(QAbstractTableModel):
|
|
"""
|
|
Modèle pour gérer l'affichage des logs dans un QTableView.
|
|
"""
|
|
|
|
def __init__(self, display_lines: int = 100):
|
|
super().__init__()
|
|
self._dataList = [] # Stocke les logs
|
|
self._maxLines = display_lines # Nombre maximum de lignes
|
|
self._headers = [_("Timestamp"), _("Level"), _("Message")]
|
|
self._sortColumn = 0
|
|
self._sortOrder = Qt.SortOrder.AscendingOrder
|
|
self.sort(self._sortColumn, self._sortOrder)
|
|
|
|
def rowCount(self, parent = QModelIndex()):
|
|
return len(self._dataList)
|
|
|
|
def columnCount(self, parent = QModelIndex()):
|
|
return len(self._headers)
|
|
|
|
def set_headers(self, new_headers: list):
|
|
self._headers = new_headers
|
|
self.headerDataChanged.emit(Qt.Orientation.Horizontal, 0, len(self._headers) - 1)
|
|
self.layoutChanged.emit()
|
|
|
|
def headerData(self, section:int, orientation:Qt.Orientation, role:Qt.ItemDataRole = Qt.ItemDataRole.DisplayRole) -> Any:
|
|
"""
|
|
Définit les en-têtes des colonnes.
|
|
"""
|
|
if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal:
|
|
return self._headers[section]
|
|
|
|
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any:
|
|
if not index.isValid():
|
|
return None
|
|
elif role == Qt.ItemDataRole.DisplayRole:
|
|
log = self._dataList[index.row()]
|
|
if index.column() == 0: # Timestamp
|
|
return log["timestamp"]
|
|
elif index.column() == 1: # Level
|
|
return log["level"]
|
|
elif index.column() == 2: # Message
|
|
return log["message"]
|
|
elif role == Qt.ItemDataRole.BackgroundRole and index.column() == 1: # Level
|
|
log = self._dataList[index.row()]
|
|
if log["level"] == 'DEBUG':
|
|
return QColor(Qt.GlobalColor.cyan)
|
|
elif log["level"] == 'INFO':
|
|
return QColor(Qt.GlobalColor.white)
|
|
elif log["level"] == 'SUCCESS':
|
|
return QColor(Qt.GlobalColor.green)
|
|
elif log["level"] == 'WARNING':
|
|
return QColor(Qt.GlobalColor.yellow)
|
|
elif log["level"] == 'ERROR':
|
|
return QColor(Qt.GlobalColor.red)
|
|
elif log["level"] == 'CRITICAL':
|
|
return QColor(Qt.GlobalColor.darkRed)
|
|
elif role == Qt.ItemDataRole.ForegroundRole and index.column() == 1: # Level
|
|
log = self._dataList[index.row()]
|
|
if log["level"] == 'DEBUG':
|
|
return QColor(Qt.GlobalColor.darkGray)
|
|
elif log["level"] == 'INFO':
|
|
return QColor(Qt.GlobalColor.black)
|
|
elif log["level"] == 'SUCCESS':
|
|
return QColor(Qt.GlobalColor.darkGray)
|
|
elif log["level"] == 'WARNING':
|
|
return QColor(Qt.GlobalColor.lightGray)
|
|
elif log["level"] == 'ERROR':
|
|
return QColor(Qt.GlobalColor.darkGray)
|
|
elif log["level"] == 'CRITICAL':
|
|
return QColor(Qt.GlobalColor.lightGray)
|
|
elif role == Qt.ItemDataRole.FontRole:
|
|
if index.column() == 1: # Level
|
|
# log = self._dataList[index.row()]
|
|
font = QFont("Helvetica")
|
|
font.setBold(True)
|
|
return font
|
|
return None
|
|
elif role == Qt.ItemDataRole.TextAlignmentRole:
|
|
global is_rtl
|
|
if index.column() == 0:
|
|
return get_qt_align_vchr()
|
|
elif index.column() == 1:
|
|
return get_qt_align_center()
|
|
elif index.column() == 2:
|
|
return get_qt_align_vchl()
|
|
return None
|
|
|
|
def sort(self, column: int, order: Qt.SortOrder = Qt.SortOrder.DescendingOrder):
|
|
self._sortColumn = column
|
|
self._sortOrder = order
|
|
# Sorting logic
|
|
if column == 0: # Sorting by "timestamp"
|
|
self._dataList.sort(key = lambda log: log["timestamp"], reverse = (order == Qt.SortOrder.AscendingOrder))
|
|
elif column == 1: # Sorting by "level"
|
|
self._dataList.sort(key = lambda log: log["level"], reverse = (order == Qt.SortOrder.AscendingOrder))
|
|
elif column == 2: # Sorting by "message"
|
|
self._dataList.sort(key = lambda log: log["message"], reverse = (order == Qt.SortOrder.AscendingOrder))
|
|
# Notify view that data has changed after sorting
|
|
self.layoutChanged.emit()
|
|
|
|
def add_data(self, log: dict):
|
|
"""
|
|
Ajoute un log au modèle tout en respectant la limite maximale des lignes.
|
|
"""
|
|
self._dataList.append(log)
|
|
while len(self._dataList) > self._maxLines:
|
|
self._dataList.pop(0) # Supprime les anciennes lignes
|
|
self.sort(self._sortColumn, self._sortOrder)
|
|
self.layoutChanged.emit()
|
|
|
|
def set_data(self, data: list):
|
|
self._dataList = data
|
|
self.sort(self._sortColumn, self._sortOrder)
|
|
self.layoutChanged.emit()
|
|
|
|
|
|
class LogViewerWidget(QWidget):
|
|
"""
|
|
Widget principal contenant un QTableView pour afficher les logs.
|
|
"""
|
|
|
|
def __init__(self, max_display_lines: int = 100, parent = None):
|
|
super().__init__(parent)
|
|
# Layout principal
|
|
self._main_Qvlo = QVBoxLayout(self)
|
|
self.setLayout(self._main_Qvlo)
|
|
self._main_Qvlo.setContentsMargins(0, 0, 0, 0)
|
|
# TableView
|
|
self._logs_Qtvmb = QTableView(self)
|
|
self._main_Qvlo.addWidget(self._logs_Qtvmb)
|
|
# Model
|
|
self._logs_lQtm = LogsTableModel(max_display_lines)
|
|
self._logs_Qtvmb.setModel(self._logs_lQtm)
|
|
# selection de ligne entiere
|
|
self._logs_Qtvmb.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
|
# désactiver tentative d'edition
|
|
self._logs_Qtvmb.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) # Lecture seule
|
|
# alterner les couleurs
|
|
self._logs_Qtvmb.setAlternatingRowColors(True)
|
|
# remplir le background
|
|
self._logs_Qtvmb.setAutoFillBackground(True)
|
|
# activer le trie
|
|
self._logs_Qtvmb.setSortingEnabled(True)
|
|
# Configuration des tailles des colonnes
|
|
self._logs_Qtvmb.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) # Première colonne
|
|
self._logs_Qtvmb.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) # Deuxième colonne
|
|
self._logs_Qtvmb.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) # Troisième colonne
|
|
self._logs_Qtvmb.setTextElideMode(Qt.TextElideMode.ElideNone)
|
|
self._logs_Qtvmb.setWordWrap(False)
|
|
# Émetteur de signaux pour les logs
|
|
self._logEmitter = LogEmitter()
|
|
self._logEmitter.newLogSignal.connect(self.add_log) # Connexion du signal
|
|
# Ajouter un gestionnaire personnalisé pour loguru
|
|
log_handler = LogHandler(self._logEmitter)
|
|
# logger.remove() # Supprime les sorties par défaut
|
|
logger.add(log_handler, format = "{time:YYYY-MM-DD HH:mm:ss.SSS} -|- {level} -|- {message}", level = "INFO")
|
|
# update display
|
|
self.update_display()
|
|
|
|
def add_log(self, log: dict):
|
|
"""
|
|
Ajoute un log au modèle et effectue le défilement automatique.
|
|
"""
|
|
self._logs_lQtm.add_data(log)
|
|
|
|
def update_display(self):
|
|
self._logs_lQtm.set_headers([_("Timestamp"), _("Level"), _("Message")])
|
|
self.setLayoutDirection(get_qt_dir())
|
|
|
|
|
|
class Wallet:
|
|
"""Class Wallet can raise ValueError """
|
|
WrongWalletName = 1
|
|
"""'walletName' wrong type (must not be empty string)"""
|
|
WrongWalletId = 2
|
|
"""'walletId' wrong type (must not be empty string)"""
|
|
WrongWalletPass = 3
|
|
"""'walletPass' wrong type (must not be empty string)"""
|
|
WrongWalletType = 4
|
|
"""'walletType' must be equal string 'solana'"""
|
|
JSONDecodeError = 5
|
|
"""Cannot decode Json"""
|
|
|
|
def __init__(self,
|
|
wallet_name: str,
|
|
wallet_pub_key: str,
|
|
wallet_priv_key: str,
|
|
dt_crea: datetime = None,
|
|
dt_update: datetime = None,
|
|
main_sol_balance: float = 0,
|
|
main_tokens: list = None,
|
|
dev_sol_balance: float = 0,
|
|
dev_tokens: list = None,
|
|
wallet_type: str = 'solana'):
|
|
# Vérification des paramètres obligatoires
|
|
if not isinstance(wallet_name, str) or not wallet_name.strip():
|
|
raise ValueError(Wallet.WrongWalletName)
|
|
if not isinstance(wallet_pub_key, str) or not wallet_pub_key.strip():
|
|
raise ValueError(Wallet.WrongWalletId)
|
|
if not isinstance(wallet_priv_key, str) or not wallet_priv_key.strip():
|
|
raise ValueError(Wallet.WrongWalletPass)
|
|
if wallet_type != "solana":
|
|
raise ValueError(Wallet.WrongWalletType)
|
|
# Attribuer les valeurs obligatoires
|
|
self.walletType = wallet_type
|
|
self.walletName = wallet_name
|
|
self.walletPubKey = wallet_pub_key
|
|
self.walletPrivKey = wallet_priv_key
|
|
# Gérer les valeurs par défaut des dates
|
|
self.dtCrea = dt_crea if isinstance(dt_crea, datetime) else datetime.now(timezone.utc)
|
|
self.dtUpdate = dt_update if isinstance(dt_update, datetime) else datetime.now(timezone.utc)
|
|
# Gérer les balances et les mettre à zéro si elles sont négatives
|
|
self.mainSolBalance = max(main_sol_balance, 0)
|
|
self.devSolBalance = max(dev_sol_balance, 0)
|
|
# Initialiser les listes de tokens si elles ne sont pas fournies
|
|
self.mainTokens = main_tokens if main_tokens is not None else []
|
|
self.devTokens = dev_tokens if dev_tokens is not None else []
|
|
|
|
def to_json(self) -> str:
|
|
"""Convertir l'instance en JSON"""
|
|
return json.dumps({
|
|
"walletType": self.walletType,
|
|
"walletName": self.walletName,
|
|
"walletPubKey": self.walletPubKey,
|
|
"walletPrivKey": self.walletPrivKey,
|
|
"dtCrea": self.dtCrea.isoformat(),
|
|
"dtUpdate": self.dtUpdate.isoformat(),
|
|
"mainSolBalance": self.mainSolBalance,
|
|
"mainTokens": self.mainTokens,
|
|
"devSolBalance": self.devSolBalance,
|
|
"devTokens": self.devTokens
|
|
})
|
|
|
|
@staticmethod
|
|
def parse_datetime(dt_str):
|
|
"""Parse une chaîne de date en datetime, retourne l'heure actuelle en UTC en cas d'erreur."""
|
|
if isinstance(dt_str, str):
|
|
try:
|
|
return datetime.fromisoformat(dt_str)
|
|
except ValueError:
|
|
logger.warning("Format de date invalide, remplacement par l'heure actuelle.")
|
|
return datetime.now(timezone.utc)
|
|
|
|
@staticmethod
|
|
def from_json(json_string: str):
|
|
"""Créer une instance Wallet à partir d'une chaîne JSON"""
|
|
try:
|
|
data = json.loads(json_string)
|
|
# Vérification des paramètres obligatoires
|
|
if 'walletName' not in data or not isinstance(data["walletName"], str) or not data["walletName"].strip():
|
|
raise ValueError(Wallet.WrongWalletName)
|
|
if 'walletPubKey' not in data or not isinstance(data["walletPubKey"], str) or not data["walletPubKey"].strip():
|
|
raise ValueError(Wallet.WrongWalletId)
|
|
if 'walletPrivKey' not in data or not isinstance(data["walletPrivKey"], str) or not data["walletPrivKey"].strip():
|
|
raise ValueError(Wallet.WrongWalletPass)
|
|
if 'walletType' not in data or data["walletType"] != "solana":
|
|
raise ValueError(Wallet.WrongWalletType)
|
|
wallet_name = data["walletName"]
|
|
wallet_pub_key = data["walletPubKey"]
|
|
wallet_priv_key = data["walletPrivKey"]
|
|
wallet_type = data["walletType"]
|
|
# Récupérer et valider les dates
|
|
dt_crea = Wallet.parse_datetime(data.get("dtCrea"))
|
|
dt_update = Wallet.parse_datetime(data.get("dtUpdate"))
|
|
# Récupérer et valider les balances, les réinitialiser à zéro si elles sont négatives
|
|
main_sol_balance = max(data.get("mainSolBalance", 0), 0)
|
|
dev_sol_balance = max(data.get("devSolBalance", 0), 0)
|
|
# Récupérer les listes de tokens, initialiser à liste vide si absentes
|
|
main_tokens = data.get("mainTokens", [])
|
|
dev_tokens = data.get("devTokens", [])
|
|
# Créer et retourner l'instance Wallet
|
|
return Wallet(wallet_name, wallet_pub_key, wallet_priv_key, dt_crea, dt_update, main_sol_balance, main_tokens, dev_sol_balance, dev_tokens, wallet_type)
|
|
except json.JSONDecodeError:
|
|
raise ValueError(Wallet.JSONDecodeError)
|
|
|
|
|
|
class WalletTokensTableModel(QAbstractTableModel):
|
|
|
|
def __init__(self, parent = None):
|
|
super().__init__(parent)
|
|
self._headers = [_("Token Id"), _("Buy Price"), _("Buy Time"), _("Action")]
|
|
self._dataList = []
|
|
|
|
def set_headers(self, new_headers: list):
|
|
self._headers = new_headers
|
|
self.headerDataChanged.emit(Qt.Orientation.Horizontal, 0, 3)
|
|
self.layoutChanged.emit()
|
|
|
|
def rowCount(self, parent = QModelIndex()):
|
|
return len(self._dataList)
|
|
|
|
def columnCount(self, parent = QModelIndex()):
|
|
return len(self._headers)
|
|
|
|
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any:
|
|
if not index.isValid():
|
|
return None
|
|
elif role == Qt.ItemDataRole.DisplayRole:
|
|
token = self._dataList[index.row()]
|
|
if index.column() == 0: # Timestamp
|
|
return token["token_id"]
|
|
elif index.column() == 1: # Level
|
|
return token["buy_price"]
|
|
elif index.column() == 2: # Message
|
|
return token["buy_time"]
|
|
elif index.column() == 3: # Action
|
|
return ""
|
|
elif role == Qt.ItemDataRole.TextAlignmentRole:
|
|
global is_rtl
|
|
if index.column() == 0:
|
|
return get_qt_align_vchr()
|
|
elif index.column() == 1:
|
|
return get_qt_align_center()
|
|
elif index.column() == 2:
|
|
return get_qt_align_vchl(not is_rtl)
|
|
elif role == Qt.ItemDataRole.DecorationRole and index.column() == 0:
|
|
return QIcon(":icoWallet")
|
|
|
|
return None
|
|
|
|
def headerData(self, section, orientation, role = Qt.ItemDataRole.DisplayRole):
|
|
if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal:
|
|
return self._headers[section]
|
|
return None
|
|
|
|
def set_data(self, data:list):
|
|
self._dataList = data
|
|
self.layoutChanged.emit()
|
|
|
|
|
|
class WalletWidget(QWidget):
|
|
|
|
def __init__(self, parent = None):
|
|
super().__init__(parent)
|
|
self.setWindowIcon(QIcon(":icoWallet"))
|
|
self.wallet = None
|
|
self.fileKey = None
|
|
self.filePath = None
|
|
self._main_Qglo = QGridLayout(self)
|
|
self.setLayout(self._main_Qglo)
|
|
# Line 1: Wallet Name
|
|
self._walletName_Qlbl = QLabel()
|
|
self.walletName_Qled = QLineEdit()
|
|
self._saveWallet_Qpbtn = QPushButton()
|
|
self._init_line1()
|
|
# Line 2: Wallet PubKey
|
|
self._walletPubKey_Qlbl = QLabel()
|
|
self.walletPubKey_Qled = QLineEdit()
|
|
self._copyToClipboard_Qpbtn = QPushButton()
|
|
self._init_line2()
|
|
# Line 3: Balances
|
|
self._realBalance_Qlbl = QLabel()
|
|
self.realBalance_Qled = QLineEdit()
|
|
self._demoBalance_Qlbl = QLabel()
|
|
self.demoBalance_Qled = QLineEdit()
|
|
self._init_line3()
|
|
# Line 4: Tokens Label
|
|
self._tokens_Qlbl = QLabel()
|
|
self._init_line4()
|
|
# # Line 5: TableView
|
|
self._tokens_qtbvmb = QTableView(self)
|
|
self._tokens_wtQtm = WalletTokensTableModel(self)
|
|
self._init_tokens_qtbvmb()
|
|
self.update_display()
|
|
self.populate_table()
|
|
|
|
def set_wallet(self, wallet: Wallet, file_pass: str, file_path:str):
|
|
self.save_wallet()
|
|
self.fileKey = file_pass
|
|
self.filePath = file_path
|
|
self.wallet = wallet
|
|
self.update_wallet_display()
|
|
|
|
def update_wallet_display(self):
|
|
if self.wallet is not None:
|
|
self.walletPubKey_Qled.setText(self.wallet.walletPubKey)
|
|
self.walletName_Qled.setText(self.wallet.walletName)
|
|
self.realBalance_Qled.setText(f"{self.wallet.mainSolBalance:.9f}")
|
|
self.demoBalance_Qled.setText(f"{self.wallet.devSolBalance:.9f}")
|
|
else:
|
|
self.walletPubKey_Qled.setText("")
|
|
self.walletName_Qled.setText("")
|
|
self.realBalance_Qled.setText(f"{0:.9f}")
|
|
self.demoBalance_Qled.setText(f"{0:.9f}")
|
|
|
|
def save_wallet(self):
|
|
if self.wallet is not None and self.fileKey is not None and self.filePath is not None:
|
|
encrypted_data = encrypt_string(self.fileKey, APP_SALT.encode(), self.wallet.to_json())
|
|
with LockedFile(self.filePath, "w+b") as file:
|
|
file.seek(0)
|
|
file.truncate()
|
|
file.write(encrypted_data.encode())
|
|
|
|
def _init_line1(self):
|
|
self.walletName_Qled.setAlignment(get_qt_align_center())
|
|
self._saveWallet_Qpbtn.setIcon(QIcon(":icoSave"))
|
|
self._saveWallet_Qpbtn.setMaximumWidth(50)
|
|
self._saveWallet_Qpbtn.clicked.connect(self.save_wallet)
|
|
self._main_Qglo.addWidget(self._walletName_Qlbl, 0, 0, 1, 2)
|
|
self._main_Qglo.addWidget(self.walletName_Qled, 0, 2, 1, 9)
|
|
self._main_Qglo.addWidget(self._saveWallet_Qpbtn, 0, 11, 1, 1)
|
|
|
|
def _init_line2(self):
|
|
self.walletPubKey_Qled.setAlignment(get_qt_align_center())
|
|
self.walletPubKey_Qled.setText("XXXXXXXXXXXX")
|
|
self.walletPubKey_Qled.setReadOnly(True)
|
|
self._copyToClipboard_Qpbtn.setIcon(QIcon(":icoCopy"))
|
|
self._copyToClipboard_Qpbtn.setMaximumWidth(50)
|
|
self._copyToClipboard_Qpbtn.clicked.connect(self.copy_wallet_priv_key_to_clipboard)
|
|
self._main_Qglo.addWidget(self._walletPubKey_Qlbl, 1, 0, 1, 2)
|
|
self._main_Qglo.addWidget(self.walletPubKey_Qled, 1, 2, 1, 9)
|
|
self._main_Qglo.addWidget(self._copyToClipboard_Qpbtn, 1, 11, 1, 1)
|
|
|
|
def _init_line3(self):
|
|
self.realBalance_Qled.setAlignment(get_qt_align_vchr())
|
|
self.realBalance_Qled.setText("10.000000000")
|
|
# self.realBalance_Qled.setMaximumWidth(300)
|
|
self.realBalance_Qled.setReadOnly(True)
|
|
self.demoBalance_Qled.setAlignment(get_qt_align_vchr())
|
|
self.demoBalance_Qled.setText("10.000000000")
|
|
# self.demoBalance_Qled.setMaximumWidth(300)
|
|
self.demoBalance_Qled.setReadOnly(True)
|
|
self._main_Qglo.addWidget(self._realBalance_Qlbl, 2, 0, 1, 2)
|
|
self._main_Qglo.addWidget(self.realBalance_Qled, 2, 2, 1, 3)
|
|
self._main_Qglo.addWidget(self._demoBalance_Qlbl, 2, 7, 1, 2)
|
|
self._main_Qglo.addWidget(self.demoBalance_Qled, 2, 9, 1, 3)
|
|
|
|
def _init_line4(self):
|
|
self._tokens_Qlbl.setAlignment(get_qt_align_center())
|
|
self._main_Qglo.addWidget(self._tokens_Qlbl, 3, 0, 1, 12)
|
|
|
|
def _init_tokens_qtbvmb(self):
|
|
self._tokens_qtbvmb.setModel(self._tokens_wtQtm)
|
|
# selection de ligne entiere
|
|
self._tokens_qtbvmb.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
|
# desactiver tentative d'edition
|
|
self._tokens_qtbvmb.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) # Lecture seule
|
|
# alterner les couleurs
|
|
self._tokens_qtbvmb.setAlternatingRowColors(True)
|
|
# remplir le background
|
|
self._tokens_qtbvmb.setAutoFillBackground(True)
|
|
# activer le trie
|
|
self._tokens_qtbvmb.setSortingEnabled(True)
|
|
|
|
# Configuration des tailles des colonnes
|
|
self._tokens_qtbvmb.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) # Première colonne
|
|
self._tokens_qtbvmb.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) # Deuxième colonne
|
|
self._tokens_qtbvmb.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Interactive) # Troisième colonne
|
|
self._tokens_qtbvmb.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch) # Quatrième colonne
|
|
self._tokens_qtbvmb.setTextElideMode(Qt.TextElideMode.ElideNone)
|
|
self._tokens_qtbvmb.setWordWrap(False)
|
|
self._main_Qglo.addWidget(self._tokens_qtbvmb, 4, 0, 1, 12)
|
|
|
|
def update_display(self):
|
|
global is_rtl
|
|
self.setLayoutDirection(get_qt_dir())
|
|
self._walletName_Qlbl.setText(_("Wallet Name:"))
|
|
self._walletName_Qlbl.setAlignment(get_qt_align_vchr(is_rtl))
|
|
self._walletPubKey_Qlbl.setText(_("Wallet PubKey:"))
|
|
self._walletPubKey_Qlbl.setAlignment(get_qt_align_vchr(is_rtl))
|
|
self._realBalance_Qlbl.setText(_("Balance Real:"))
|
|
self._realBalance_Qlbl.setAlignment(get_qt_align_vchr(is_rtl))
|
|
self._demoBalance_Qlbl.setText(_("Balance Demo:"))
|
|
self._demoBalance_Qlbl.setAlignment(get_qt_align_vchr(is_rtl))
|
|
self._tokens_Qlbl.setText(_("My Tokens:"))
|
|
self._tokens_wtQtm.set_headers([_("Token Id"), _("Buy Price"), _("Buy Time"), _("Action")])
|
|
|
|
def populate_table(self):
|
|
data = []
|
|
for x in range(10):
|
|
token_id = ''.join(random.choices('ABCDEFGHIJKLMNOPQRSTUVWXYZ', k = 3))
|
|
buy_price = f"{random.uniform(0.000000001, 1.000000000):.9f}"
|
|
buy_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
|
action = _("Sell") # Placeholder for button-like behavior
|
|
|
|
token = {
|
|
"token_id": token_id, # token_id
|
|
"buy_price": buy_price, # Niveau du log
|
|
"buy_time": buy_time, # Message
|
|
"action": action # Message
|
|
}
|
|
|
|
data.append(token)
|
|
|
|
self._tokens_wtQtm.set_data(data)
|
|
|
|
def copy_wallet_priv_key_to_clipboard(self):
|
|
"""Copier le texte de QLineEdit dans le presse-papiers."""
|
|
clipboard = QApplication.clipboard()
|
|
clipboard.setText(self.walletPubKey_Qled.text())
|
|
logger.info(_("walletId copied to clipboard"))
|
|
|
|
|
|
class SimpleMsgBox(QMessageBox):
|
|
def showEvent(self, event) -> None:
|
|
super().showEvent(event)
|
|
center_on_screen(self)
|
|
|
|
def __init__(self, window_title: str, window_text: str, ico: QMessageBox.Icon, parent = None):
|
|
super().__init__(parent)
|
|
self.setLayoutDirection(get_qt_dir())
|
|
self.setOption(QMessageBox.Option.DontUseNativeDialog, True)
|
|
self.setWindowIcon(QIcon(":imgIcon"))
|
|
self.setWindowTitle(window_title)
|
|
self.setText(window_text)
|
|
self.setIcon(ico)
|
|
|
|
|
|
class NewWalletNameAndPassDialog(QDialog):
|
|
|
|
def showEvent(self, event) -> None:
|
|
super().showEvent(event)
|
|
center_on_screen(self)
|
|
|
|
def __init__(self, parent = None):
|
|
super().__init__(parent)
|
|
self.setLayoutDirection(get_qt_dir())
|
|
self.setWindowIcon(QIcon(":imgIcon"))
|
|
self.setWindowTitle(_("Wallet Name and EncryptPass"))
|
|
main_qflo = QFormLayout()
|
|
self.setLayout(main_qflo)
|
|
self._walletName_Qle = QLineEdit(self)
|
|
self._walletEncyptPass_Qle = QLineEdit(self)
|
|
self._walletEncyptPass_Qle.setEchoMode(QLineEdit.EchoMode.Password)
|
|
main_qflo.addRow(_("Wallet Name:"), self._walletName_Qle)
|
|
main_qflo.addRow(_("Wallet EncryptPass:"), self._walletEncyptPass_Qle)
|
|
|
|
validate_qpbtn = QPushButton(_("Validate"))
|
|
cancel_qpbtn = QPushButton(_("Cancel"))
|
|
|
|
button_qdbbox = QDialogButtonBox(self)
|
|
button_qdbbox.addButton(validate_qpbtn, QDialogButtonBox.ButtonRole.AcceptRole)
|
|
button_qdbbox.addButton(cancel_qpbtn, QDialogButtonBox.ButtonRole.RejectRole)
|
|
|
|
# Connecter les signaux des boutons
|
|
validate_qpbtn.clicked.connect(self.validate_and_accept)
|
|
cancel_qpbtn.clicked.connect(self.reject)
|
|
|
|
main_qflo.addWidget(button_qdbbox)
|
|
|
|
self.walletName = None
|
|
self.walletEncyptPass = None
|
|
|
|
def validate_and_accept(self):
|
|
# Validation du champ 1
|
|
if len(self._walletName_Qle.text().strip()) < 2:
|
|
msg_qmbox = SimpleMsgBox(_("Error"), _("The Wallet Name must have a minimum of 2 characters."), QMessageBox.Icon.Warning, self)
|
|
msg_qmbox.addButton(_("Ok"), QMessageBox.ButtonRole.ActionRole)
|
|
msg_qmbox.exec()
|
|
return
|
|
# Validation du champ 2 (mot de passe)
|
|
if not re.match(r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$", self._walletEncyptPass_Qle.text()):
|
|
msg_qmbox = SimpleMsgBox(_("Error"), _("The Wallet EncryptPass must have a minimum of 1 lower case character, 1 upper case character, 1 numeric character, 1 special character (@$!%*?&), and must have a minimum of 8 characters length."), QMessageBox.Icon.Warning, self)
|
|
msg_qmbox.addButton(_("Ok"), QMessageBox.ButtonRole.ActionRole)
|
|
msg_qmbox.exec()
|
|
return
|
|
self.walletName = self._walletName_Qle.text().strip()
|
|
self.walletEncyptPass = self._walletEncyptPass_Qle.text()
|
|
# Si les deux champs sont valides, on accepte la saisie
|
|
self.accept()
|
|
|
|
|
|
class NewWalletFileDialog(QFileDialog):
|
|
|
|
def showEvent(self, event) -> None:
|
|
super().showEvent(event)
|
|
center_on_screen(self)
|
|
|
|
def __init__(self, parent = None):
|
|
super().__init__(parent)
|
|
self.setOption(QFileDialog.Option.DontUseNativeDialog, True) # Empêche d'écraser un fichier existant
|
|
self.setWindowTitle(_("Save Content to New Wallet File"))
|
|
self.setWindowIcon(QIcon(":imgIcon"))
|
|
self.setAcceptMode(QFileDialog.AcceptMode.AcceptSave) # Mode "Enregistrer sous"
|
|
self.setFileMode(QFileDialog.FileMode.AnyFile) # Permet de saisir un nouveau fichier
|
|
self.setOption(QFileDialog.Option.DontConfirmOverwrite, True)
|
|
self.setLabelText(QFileDialog.DialogLabel.LookIn, _("Wallets location:"))
|
|
self.setLabelText(QFileDialog.DialogLabel.FileName, _("File name:"))
|
|
self.setLabelText(QFileDialog.DialogLabel.FileType, "File type:")
|
|
self.setLabelText(QFileDialog.DialogLabel.Accept, _("Validate"))
|
|
self.setLabelText(QFileDialog.DialogLabel.Reject, _("Cancel"))
|
|
self.setDirectory(WALLETS_FOLDER)
|
|
self.setDefaultSuffix(WALLET_FILE_EXT)
|
|
self.setNameFilter(_("%(desc)s (*.%(ext)s)") % {"desc": WALLET_FILE_DESC, "ext": WALLET_FILE_EXT})
|
|
# self.setMinimumSize(dialog.width(), dialog.height())
|
|
|
|
|
|
class LoadWalletPassDialog(QDialog):
|
|
|
|
def showEvent(self, event) -> None:
|
|
super().showEvent(event)
|
|
center_on_screen(self)
|
|
|
|
def __init__(self, file_name: str, parent = None):
|
|
super().__init__(parent)
|
|
self.setLayoutDirection(get_qt_dir())
|
|
self.setWindowIcon(QIcon(":imgIcon"))
|
|
self.setWindowTitle(_("Wallet EncryptPass for file '%(fileName)s'") % {"fileName": file_name})
|
|
main_qflo = QFormLayout()
|
|
self.setLayout(main_qflo)
|
|
self._walletEncyptPass_Qle = QLineEdit(self)
|
|
self._walletEncyptPass_Qle.setEchoMode(QLineEdit.EchoMode.Password)
|
|
main_qflo.addRow(_("Wallet EncryptPass:"), self._walletEncyptPass_Qle)
|
|
|
|
validate_qpbtn = QPushButton(_("Validate"))
|
|
cancel_qpbtn = QPushButton(_("Cancel"))
|
|
button_qdbbox = QDialogButtonBox(self)
|
|
button_qdbbox.addButton(validate_qpbtn, QDialogButtonBox.ButtonRole.AcceptRole)
|
|
button_qdbbox.addButton(cancel_qpbtn, QDialogButtonBox.ButtonRole.RejectRole)
|
|
# Connecter les signaux des boutons
|
|
validate_qpbtn.clicked.connect(self.validate_and_accept)
|
|
cancel_qpbtn.clicked.connect(self.reject)
|
|
main_qflo.addWidget(button_qdbbox)
|
|
self.walletEncyptPass = None
|
|
|
|
def validate_and_accept(self):
|
|
# Validation du champ 1
|
|
if len(self._walletEncyptPass_Qle.text().strip()) < 1:
|
|
msg_qmbox = SimpleMsgBox(_("Error"), _("You must enter a valid Password."), QMessageBox.Icon.Warning, self)
|
|
msg_qmbox.addButton(_("Ok"), QMessageBox.ButtonRole.ActionRole)
|
|
msg_qmbox.exec()
|
|
return
|
|
self.walletEncyptPass = self._walletEncyptPass_Qle.text()
|
|
# Si les deux champs sont valides, on accepte la saisie
|
|
self.accept()
|
|
|
|
|
|
class LoadWalletFileDialog(QFileDialog):
|
|
|
|
def showEvent(self, event) -> None:
|
|
super().showEvent(event)
|
|
center_on_screen(self)
|
|
|
|
def __init__(self, parent = None):
|
|
super().__init__(parent)
|
|
self.setOption(QFileDialog.Option.DontUseNativeDialog, True) # Empêche d'écraser un fichier existant
|
|
self.setWindowTitle(_("Load Wallet File"))
|
|
self.setWindowIcon(QIcon(":imgIcon"))
|
|
self.setAcceptMode(QFileDialog.AcceptMode.AcceptOpen) # Mode "Ouvrir sous"
|
|
self.setFileMode(QFileDialog.FileMode.ExistingFile) # Permet de saisir un fichier existant
|
|
self.setDirectory(WALLETS_FOLDER)
|
|
self.setDefaultSuffix(WALLET_FILE_EXT)
|
|
self.setLabelText(QFileDialog.DialogLabel.LookIn, _("Wallets location:"))
|
|
self.setLabelText(QFileDialog.DialogLabel.FileName, _("File name:"))
|
|
self.setLabelText(QFileDialog.DialogLabel.FileType, "File type:")
|
|
self.setLabelText(QFileDialog.DialogLabel.Accept, _("Validate"))
|
|
self.setLabelText(QFileDialog.DialogLabel.Reject, _("Cancel"))
|
|
self.setNameFilter(_("%(desc)s (*.%(ext)s)") % {"desc": WALLET_FILE_DESC, "ext": WALLET_FILE_EXT})
|
|
# self.setMinimumSize(dialog.width(), dialog.height())
|
|
|
|
|
|
class MainWindow(QMainWindow):
|
|
|
|
def closeEvent(self, event: QEvent):
|
|
"""Intercepter la fermeture pour masquer la fenêtre au lieu de quitter."""
|
|
self.hide()
|
|
event.ignore()
|
|
|
|
def showEvent(self, event) -> None:
|
|
self.showApp_Qact.setDisabled(True)
|
|
self.hideApp_Qact.setDisabled(False)
|
|
super().showEvent(event)
|
|
if self.mustCenter:
|
|
self.mustCenter = False
|
|
center_on_screen(self)
|
|
|
|
def hideEvent(self, event) -> None:
|
|
self.showApp_Qact.setDisabled(False)
|
|
self.hideApp_Qact.setDisabled(True)
|
|
self.mustCenter = True
|
|
super().hideEvent(event)
|
|
|
|
def __init__(self, cfg:dict = DEFAULT_YAML_CONFIG):
|
|
super().__init__()
|
|
self.config = cfg
|
|
self.currentLang = get_lang_or_fallback(self.config['defaultLang'])
|
|
self.setWindowIcon(QIcon(":imgIcon"))
|
|
self.setWindowTitle(APP_NAME_VERSION)
|
|
self.setMinimumSize(1024, 768)
|
|
self.resize(max(self.config.get('lastWidth'), 1024), max(self.config.get('lastHeight'), 768))
|
|
self.mustCenter = True
|
|
# Main Menubar
|
|
self.main_Qmnub = self.menuBar()
|
|
# File Menu
|
|
self.file_Qmnu = QMenu()
|
|
self.newWallet_Qact = QAction(self)
|
|
self.newWallet_Qact.setIcon(QIcon(":icoWalletNew"))
|
|
self.openWallet_Qact = QAction(self)
|
|
self.openWallet_Qact.setIcon(QIcon(":icoWalletOpen"))
|
|
self.filesHistory_Qmnu = QMenu()
|
|
self.filesHistory_Qmnu.setIcon(QIcon(":icoLastFiles"))
|
|
self.noFileHistory_Qact = QAction(self)
|
|
self.noFileHistory_Qact.setIcon(QIcon(":icoNoHistory"))
|
|
self.noFileHistory_Qact.setDisabled(True)
|
|
self.hideApp_Qact = QAction(self)
|
|
self.hideApp_Qact.setIcon(QIcon(":icoTrayHide"))
|
|
self.closeApp_Qact = QAction(self)
|
|
self.closeApp_Qact.setIcon(QIcon(":icoQuit"))
|
|
self.forceCloseApp_Qact = QAction(self)
|
|
self.forceCloseApp_Qact.setIcon(QIcon(":icoForceQuit"))
|
|
# finalize initialisation of File Menu
|
|
self._init_file_qmnu()
|
|
# Langs Menu
|
|
self.langs_Qmnu = QMenu()
|
|
self.changeLangEn_Qact = QAction(QIcon(":icoLang"), "English", self)
|
|
self.changeLangFr_Qact = QAction(QIcon(":icoLang"), "Français", self)
|
|
self.changeLangAr_Qact = QAction(QIcon(":icoLang"), "العربية", self)
|
|
self.changeLangDe_Qact = QAction(QIcon(":icoLang"), "Deutsch", self)
|
|
self.changeLangIt_Qact = QAction(QIcon(":icoLang"), "Italiano", self)
|
|
self.changeLangEs_Qact = QAction(QIcon(":icoLang"), "Espagnol", self)
|
|
# finalize initialisation of Langs Menu
|
|
self._init_langs_qmnu()
|
|
# Help Menu
|
|
self.help_Qmnu = QMenu()
|
|
self.help_Qact = QAction(self)
|
|
self.help_Qact.setIcon(QIcon(":icoHelp"))
|
|
self.about_Qact = QAction(self)
|
|
self.about_Qact.setIcon(QIcon(":icoAbout"))
|
|
self._init_help_qmnu()
|
|
# set texts and titles for Main Menubar
|
|
# Main Toolbar
|
|
self.main_Qtb = QToolBar()
|
|
self._init_main_toolbar()
|
|
# Initialisation de l'icône du System Tray
|
|
self.tray_Qsti = QSystemTrayIcon(QIcon(":imgIcon"), self)
|
|
# self.trayIcon.setToolTip(f"{APP_FULL_NAME} ({APP_VERSION})") # don't work on linux ???
|
|
# Menu du trayIcon
|
|
self.tray_Qmnu = QMenu()
|
|
self.showApp_Qact = QAction(self)
|
|
self._init_tray_qmnu()
|
|
# Central widget: QTabWidget
|
|
self.main_Qtabw = QTabWidget(self)
|
|
self.setCentralWidget(self.main_Qtabw)
|
|
self.token_qw = QWidget()
|
|
self.main_Qtabw.addTab(self.token_qw, QIcon(":icoTokens"), _("Tokens:"))
|
|
self.token_qw.setLayout(QVBoxLayout())
|
|
self.token_qw.layout().addWidget(QTableView())
|
|
self.wallet_Qw = WalletWidget(self)
|
|
self.main_Qtabw.addTab(self.wallet_Qw, QIcon(":icoWallet"), _("Wallet: %(walletName)s") % {"walletName": ""})
|
|
self.logViewer_Qw = LogViewerWidget(1000, self)
|
|
self.main_Qtabw.addTab(self.logViewer_Qw, QIcon(":icoLogs"), _("Logs:"))
|
|
self.update_display()
|
|
logger.success(_("Init of %(appName)s") % {"appName": APP_ABOUT_NAME})
|
|
|
|
def first_load(self):
|
|
# logger.debug(type(config))
|
|
configLastMaximized = self.config.get('lastMaximized', True)
|
|
if configLastMaximized:
|
|
self.showMaximized()
|
|
else:
|
|
self.show()
|
|
lastFile = self.config["lastFile"]
|
|
if lastFile is None or lastFile == "":
|
|
self._init_new_or_load_wallet()
|
|
elif not os.path.exists(lastFile):
|
|
msg_qmbox = SimpleMsgBox(_("Error"), _("The File '%(fileName)s' does not exists. Please choose a new file.") % {"fileName": lastFile}, QMessageBox.Icon.Warning, self)
|
|
msg_qmbox.addButton(_("Ok"), QMessageBox.ButtonRole.ActionRole)
|
|
msg_qmbox.exec()
|
|
self._init_new_or_load_wallet()
|
|
else:
|
|
self._ask_load_wallet_pass(lastFile, True)
|
|
|
|
def _quit_app_func(self):
|
|
msg_qmbox = SimpleMsgBox(_("Do you really want to Quit?"), _("If you accept, all the files will be saved\nand the application will be closed."), QMessageBox.Icon.Question, self)
|
|
yes_qpbtn = msg_qmbox.addButton(_("Yes"), QMessageBox.ButtonRole.ActionRole)
|
|
msg_qmbox.addButton(_("No"), QMessageBox.ButtonRole.ActionRole) # no_qpbtn = msg_qmbox.addButton(_("No"), QMessageBox.ButtonRole.ActionRole)
|
|
msg_qmbox.exec()
|
|
if msg_qmbox.clickedButton() == yes_qpbtn:
|
|
self.config['lastMaximized'] = self.isMaximized()
|
|
# if self.isMaximized():
|
|
# self.showNormal()
|
|
# TODO: save size of non maximized window
|
|
self.config['lastHeight'] = self.height()
|
|
self.config['lastWidth'] = self.width()
|
|
# saving self.currentLang in config memory
|
|
self.config['defaultLang'] = self.currentLang
|
|
# save memory config to file
|
|
save_yaml_app_config(self.config)
|
|
|
|
self.wallet_Qw.save_wallet()
|
|
# close instance
|
|
QApplication.instance().quit()
|
|
|
|
def _force_quit_app_func(self):
|
|
msg_qmbox = SimpleMsgBox(_("Do you really want to Quit?"), _("If you accept, nothing will be saved\nand the application will be closed."), QMessageBox.Icon.Critical, self)
|
|
yes_qpbtn = msg_qmbox.addButton(_("Yes"), QMessageBox.ButtonRole.ActionRole)
|
|
msg_qmbox.addButton(_("No"), QMessageBox.ButtonRole.ActionRole) # no_qpbtn = msg_qmbox.addButton(_("No"), QMessageBox.ButtonRole.ActionRole)
|
|
msg_qmbox.exec()
|
|
if msg_qmbox.clickedButton() == yes_qpbtn:
|
|
QApplication.instance().quit()
|
|
|
|
def _change_lang(self, lang_code, lang_name):
|
|
|
|
self.currentLang = get_lang_or_fallback(lang_code)
|
|
set_app_lang_code(self.currentLang)
|
|
logger.info(_("Switching to language %(lang)s.") % {"lang": lang_name})
|
|
self.update_display()
|
|
|
|
|
|
|
|
def _init_tray_qmnu(self):
|
|
self.tray_Qsti.setContextMenu(self.tray_Qmnu)
|
|
self.tray_Qmnu.addAction(self.hideApp_Qact)
|
|
self.showApp_Qact.setIcon(QIcon(":icoTrayShow"))
|
|
self.showApp_Qact.setDisabled(True)
|
|
self.showApp_Qact.triggered.connect(self.show)
|
|
self.tray_Qmnu.addAction(self.showApp_Qact)
|
|
self.tray_Qmnu.addAction(self.closeApp_Qact)
|
|
self.tray_Qmnu.addAction(self.forceCloseApp_Qact)
|
|
|
|
self.tray_Qsti.show()
|
|
|
|
def _show_about_window(self):
|
|
"""Affiche une boîte de dialogue 'À propos'."""
|
|
bg_txt = f"<h3>{APP_FULL_NAME}: {APP_VERSION}<br>PYTHON: {platform.python_version()}<br>Qt: {pyside6version}<br>OS: {platform.system()}</h3>"
|
|
about_window = AboutWindow(bg_txt, self)
|
|
about_window.show()
|
|
|
|
def _show_help_window(self):
|
|
"""Affiche une boîte de dialogue 'Aide'."""
|
|
help_window = HelpWindow(":pdfHelp", self)
|
|
help_window.show()
|
|
|
|
def _init_main_toolbar(self):
|
|
# Création de la barre d'outils
|
|
self.addToolBar(self.main_Qtb)
|
|
self.main_Qtb.setMovable(False)
|
|
favicon_toolbar_qlbl = QLabel()
|
|
favicon_toolbar_qlbl.setPixmap(QPixmap(":imgFavicon").scaled(24, 24, Qt.AspectRatioMode.KeepAspectRatio))
|
|
self.main_Qtb.addWidget(favicon_toolbar_qlbl)
|
|
self.main_Qtb.addSeparator()
|
|
self.main_Qtb.addAction(self.openWallet_Qact)
|
|
self.main_Qtb.addAction(self.newWallet_Qact)
|
|
self.main_Qtb.addSeparator()
|
|
# self.toolbarPcp.addAction(self.actionOpenWsMainNetConnexion)
|
|
|
|
def _init_new_or_load_wallet(self):
|
|
msg_qmbox = SimpleMsgBox(_("Action Required"), _("To use the application, you have to load a wallet or to create a new one."), QMessageBox.Icon.Critical, self)
|
|
load_qpbtn = msg_qmbox.addButton(_("Load Wallet"), QMessageBox.ButtonRole.ActionRole)
|
|
create_qpbtn = msg_qmbox.addButton(_("New Wallet"), QMessageBox.ButtonRole.ActionRole)
|
|
msg_qmbox.addButton(_("Quit"), QMessageBox.ButtonRole.RejectRole) # quit_qpbtn = msg_qmbox.addButton(_("No"), QMessageBox.ButtonRole.ActionRole)
|
|
msg_qmbox.exec()
|
|
if msg_qmbox.clickedButton() == load_qpbtn:
|
|
create_wallets_folder()
|
|
self._ask_load_wallet_filepath(True)
|
|
elif msg_qmbox.clickedButton() == create_qpbtn:
|
|
create_wallets_folder()
|
|
self._ask_new_wallet_name_and_pass(True)
|
|
else:
|
|
self.close()
|
|
QApplication.instance().quit()
|
|
return
|
|
|
|
def _create_new_wallet(self):
|
|
create_wallets_folder()
|
|
self._ask_new_wallet_name_and_pass()
|
|
|
|
def _ask_new_wallet_name_and_pass(self, return_to_prev: bool = False):
|
|
wallet_dialog = NewWalletNameAndPassDialog(self)
|
|
if wallet_dialog.exec():
|
|
new_wallet_name = wallet_dialog.walletName
|
|
new_wallet_pass = wallet_dialog.walletEncyptPass
|
|
self._ask_new_wallet_filepath(new_wallet_name, new_wallet_pass, return_to_prev)
|
|
elif return_to_prev:
|
|
self._init_new_or_load_wallet()
|
|
|
|
def _ask_new_wallet_filepath(self, new_wallet_name: str, new_wallet_pass: str, return_to_prev: bool = False):
|
|
saved_file = False
|
|
while True:
|
|
saved_file = False
|
|
# Ouvre le QFileDialog pour choisir un emplacement et un nom de fichier
|
|
dialog = NewWalletFileDialog(self)
|
|
if dialog.exec():
|
|
file_path = dialog.selectedFiles()[0]
|
|
# Ajoute l'extension .kew si elle est manquante
|
|
if not file_path.endswith("." + WALLET_FILE_EXT):
|
|
file_path += "." + WALLET_FILE_EXT
|
|
file_name = os.path.basename(file_path)
|
|
# Vérifie si le fichier existe déjà
|
|
if os.path.exists(file_path):
|
|
msg_qmbox = SimpleMsgBox(_("Error"), _("The File '%(fileName)s' already exists. Please choose a new file.") % {"fileName": file_name}, QMessageBox.Icon.Warning, self)
|
|
msg_qmbox.addButton(_("Ok"), QMessageBox.ButtonRole.ActionRole)
|
|
msg_qmbox.exec()
|
|
continue # Relance le dialogue pour forcer un nouveau fichier
|
|
# Enregistre le contenu dans le nouveau fichier
|
|
try:
|
|
keypair = Keypair()
|
|
wallet = Wallet(wallet_name = new_wallet_name, wallet_pub_key = str(keypair.pubkey()), wallet_priv_key = str(keypair.secret()))
|
|
encrypted_data = encrypt_string(new_wallet_pass, APP_SALT.encode(), wallet.to_json())
|
|
with LockedFile(file_path, "w+b") as file:
|
|
file.write(encrypted_data.encode())
|
|
file_path = os.path.realpath(file_path)
|
|
if file_path not in self.config["lastFiles"]:
|
|
self.config["lastFiles"].append(file_path)
|
|
while len(self.config["lastFiles"]) > 5:
|
|
self.config["lastFiles"].pop(0)
|
|
self._refresh_files_history_qmnu()
|
|
self.config["lastFile"] = file_path
|
|
self.wallet_Qw.set_wallet(wallet, new_wallet_pass, file_path)
|
|
self.main_Qtabw.setTabText(1, _("Wallet: %(walletName)s") % {"walletName": wallet.walletName})
|
|
saved_file = True
|
|
break # Quitte la boucle après avoir sauvegardé
|
|
except Exception as e:
|
|
logger.error(e)
|
|
msg_qmbox = SimpleMsgBox(_("Error"), _("An error occurred while saving the file '%(fileName)s'.") % {"fileName": file_name}, QMessageBox.Icon.Warning, self)
|
|
msg_qmbox.addButton(_("Ok"), QMessageBox.ButtonRole.ActionRole)
|
|
msg_qmbox.exec()
|
|
break # Quitte la boucle en cas d'erreur
|
|
else:
|
|
break # L'utilisateur a annulé.
|
|
if saved_file:
|
|
return
|
|
if return_to_prev:
|
|
self._ask_new_wallet_name_and_pass(return_to_prev)
|
|
|
|
def _load_wallet(self):
|
|
self._ask_load_wallet_filepath()
|
|
|
|
def _ask_load_wallet_filepath(self, return_to_prev: bool = False):
|
|
loaded_file = False
|
|
file_path = None
|
|
while True:
|
|
dialog = LoadWalletFileDialog(self)
|
|
if dialog.exec():
|
|
file_path = dialog.selectedFiles()[0]
|
|
file_name = os.path.basename(file_path)
|
|
if not os.path.exists(file_path):
|
|
msg_qmbox = SimpleMsgBox(_("Error"), _("The File '%(fileName)s' does not exists. Please choose another file.") % {"fileName": file_name}, QMessageBox.Icon.Warning, self)
|
|
msg_qmbox.addButton(_("Ok"), QMessageBox.ButtonRole.ActionRole)
|
|
msg_qmbox.exec()
|
|
elif self.config["lastFile"] is None or file_path != self.config["lastFile"] or return_to_prev:
|
|
loaded_file = True
|
|
else:
|
|
msg_qmbox = SimpleMsgBox(_("Error"), _("The File '%(fileName)s' is already loaded. Please choose another file.") % {"fileName": file_name}, QMessageBox.Icon.Warning, self)
|
|
msg_qmbox.addButton(_("Ok"), QMessageBox.ButtonRole.ActionRole)
|
|
msg_qmbox.exec()
|
|
continue # Relance le dialogue pour forcer un nouveau fichier
|
|
if loaded_file:
|
|
break
|
|
# Relance le dialogue pour forcer un nouveau fichier
|
|
continue
|
|
else:
|
|
break # L'utilisateur a annulé.
|
|
if loaded_file:
|
|
self._ask_load_wallet_pass(file_path, return_to_prev)
|
|
return
|
|
if return_to_prev:
|
|
self._init_new_or_load_wallet()
|
|
|
|
def _ask_load_wallet_pass(self, file_path: str, return_to_prev: bool = False):
|
|
loaded_file = False
|
|
wallet = None
|
|
wallet_pass = None
|
|
while not loaded_file:
|
|
file_name = os.path.basename(file_path)
|
|
wallet_dialog = LoadWalletPassDialog(file_name, self)
|
|
if wallet_dialog.exec():
|
|
wallet_pass = wallet_dialog.walletEncyptPass
|
|
try:
|
|
with LockedFile(file_path, "rb") as file:
|
|
enc_data = file.read()
|
|
wallet = Wallet.from_json(decrypt_string(wallet_pass, APP_SALT.encode(), enc_data.decode()))
|
|
loaded_file = True
|
|
break
|
|
except portalocker.exceptions.LockException as e:
|
|
logger.error(_("LockException for Wallet file %(fileName)s %(exp)s") % {"fileName": file_path, "exp": e})
|
|
msg_qmbox = SimpleMsgBox(_("Error"), _("The File '%(fileName)s' seems to be locked. Please choose another file.") % {"fileName": file_name}, QMessageBox.Icon.Critical, self)
|
|
msg_qmbox.addButton(_("Ok"), QMessageBox.ButtonRole.ActionRole)
|
|
msg_qmbox.exec()
|
|
loaded_file = False
|
|
break
|
|
except InvalidToken as e:
|
|
logger.error(_("Wrong password for Wallet file %(fileName)s %(exp)s") % {"fileName": file_path, "exp": e})
|
|
msg_qmbox = SimpleMsgBox(_("Error"), _("The password you provided for file '%(fileName)s' is incorrect. Please try again.") % {"fileName": file_name}, QMessageBox.Icon.Critical, self)
|
|
msg_qmbox.addButton(_("Ok"), QMessageBox.ButtonRole.ActionRole)
|
|
msg_qmbox.exec()
|
|
loaded_file = False
|
|
continue
|
|
except json.JSONDecodeError as e:
|
|
logger.error(_("Cannot decode json in Wallet file %(fileName)s %(exp)s") % {"fileName": file_path, "exp": e})
|
|
msg_qmbox = SimpleMsgBox(_("Error"), _("The file '%(fileName)s' seems to be corrupted. Please choose another file.") % {"fileName": file_name}, QMessageBox.Icon.Critical, self)
|
|
msg_qmbox.addButton(_("Ok"), QMessageBox.ButtonRole.ActionRole)
|
|
msg_qmbox.exec()
|
|
loaded_file = False
|
|
break
|
|
except ValueError as e:
|
|
logger.error(_("Wrong values in %(fileName)s %(exp)s") % {"fileName": file_path, "exp": e})
|
|
msg_qmbox = SimpleMsgBox(_("Error"), _("The file '%(fileName)s' seems to have wrong values. Please choose another file.") % {"fileName": file_name}, QMessageBox.Icon.Critical, self)
|
|
msg_qmbox.addButton(_("Ok"), QMessageBox.ButtonRole.ActionRole)
|
|
msg_qmbox.exec()
|
|
loaded_file = False
|
|
break
|
|
else:
|
|
break
|
|
if loaded_file:
|
|
file_path = os.path.realpath(file_path)
|
|
if file_path not in self.config["lastFiles"]:
|
|
self.config["lastFiles"].append(file_path)
|
|
while len(self.config["lastFiles"]) > 5:
|
|
self.config["lastFiles"].pop(0)
|
|
self._refresh_files_history_qmnu()
|
|
self.config["lastFile"] = file_path
|
|
self.wallet_Qw.set_wallet(wallet, wallet_pass, file_path)
|
|
self.main_Qtabw.setTabText(1, _("Wallet: %(walletName)s") % {"walletName": wallet.walletName})
|
|
return
|
|
if return_to_prev:
|
|
self._ask_load_wallet_filepath(return_to_prev)
|
|
|
|
def _init_file_qmnu(self):
|
|
"""Initialize File Menu."""
|
|
# add File Menu to Main MenuBar
|
|
self.main_Qmnub.addMenu(self.file_Qmnu)
|
|
# add action "New Wallet" to File Menu
|
|
self.file_Qmnu.addAction(self.newWallet_Qact)
|
|
# set action target
|
|
self.newWallet_Qact.triggered.connect(self._create_new_wallet)
|
|
# add action "Open Wallet" to File Menu
|
|
self.file_Qmnu.addAction(self.openWallet_Qact)
|
|
# set action target
|
|
self.openWallet_Qact.triggered.connect(self._load_wallet)
|
|
self.file_Qmnu.addSeparator()
|
|
# add files
|
|
self.file_Qmnu.addMenu(self.filesHistory_Qmnu)
|
|
self._refresh_files_history_qmnu()
|
|
self.file_Qmnu.addSeparator()
|
|
# add action "Hide application" to File Menu
|
|
self.file_Qmnu.addAction(self.hideApp_Qact)
|
|
# set action target
|
|
self.hideApp_Qact.triggered.connect(self.hide)
|
|
# add action "Close application" to File Menu
|
|
self.file_Qmnu.addAction(self.closeApp_Qact)
|
|
# set action target
|
|
self.closeApp_Qact.triggered.connect(self._quit_app_func)
|
|
# add action "Close without saving" to File Menu
|
|
self.file_Qmnu.addAction(self.forceCloseApp_Qact)
|
|
# set action target
|
|
self.forceCloseApp_Qact.triggered.connect(self._force_quit_app_func)
|
|
|
|
def _refresh_files_history_qmnu(self):
|
|
self.filesHistory_Qmnu.clear()
|
|
for file_path in self.config.get('lastFiles', []):
|
|
if not os.path.exists(file_path):
|
|
self.config['lastFiles'].remove(file_path)
|
|
else:
|
|
file_name = os.path.basename(file_path)
|
|
file_open_qact = QAction(QIcon(":icoWallet"), file_name, self)
|
|
self.filesHistory_Qmnu.addAction(file_open_qact)
|
|
# TODO: Add trigger connect
|
|
else:
|
|
self.filesHistory_Qmnu.addAction(self.noFileHistory_Qact)
|
|
|
|
def _init_langs_qmnu(self):
|
|
"""Initialize Langs Menu."""
|
|
self.main_Qmnub.addMenu(self.langs_Qmnu)
|
|
#
|
|
self.langs_Qmnu.addAction(self.changeLangEn_Qact)
|
|
self.changeLangEn_Qact.triggered.connect(lambda: self._change_lang("en", self.changeLangEn_Qact.text()))
|
|
#
|
|
self.langs_Qmnu.addAction(self.changeLangFr_Qact)
|
|
self.changeLangFr_Qact.triggered.connect(lambda: self._change_lang("fr", self.changeLangFr_Qact.text()))
|
|
#
|
|
self.langs_Qmnu.addAction(self.changeLangAr_Qact)
|
|
self.changeLangAr_Qact.triggered.connect(lambda: self._change_lang("ar", self.changeLangAr_Qact.text()))
|
|
# self.changeLangAr_Qact.setDisabled(True)
|
|
#
|
|
self.langs_Qmnu.addAction(self.changeLangDe_Qact)
|
|
self.changeLangDe_Qact.triggered.connect(lambda: self._change_lang("de", self.changeLangDe_Qact.text()))
|
|
self.changeLangDe_Qact.setDisabled(True)
|
|
#
|
|
self.langs_Qmnu.addAction(self.changeLangIt_Qact)
|
|
self.changeLangIt_Qact.triggered.connect(lambda: self._change_lang("it", self.changeLangIt_Qact.text()))
|
|
self.changeLangIt_Qact.setDisabled(True)
|
|
#
|
|
self.langs_Qmnu.addAction(self.changeLangEs_Qact)
|
|
self.changeLangEs_Qact.triggered.connect(lambda: self._change_lang("es", self.changeLangEs_Qact.text()))
|
|
self.changeLangEs_Qact.setDisabled(True)
|
|
|
|
def _init_help_qmnu(self):
|
|
"""Initialize Help menu."""
|
|
# add Help Menu to Main MenuBar
|
|
self.main_Qmnub.addMenu(self.help_Qmnu)
|
|
# add action "Help Content" to Help Menu
|
|
self.help_Qmnu.addAction(self.help_Qact)
|
|
# set action target
|
|
self.help_Qact.triggered.connect(self._show_help_window)
|
|
# add action "About Khadhroony SRLPv4" to Help Menu
|
|
self.help_Qmnu.addAction(self.about_Qact)
|
|
# set action target
|
|
self.about_Qact.triggered.connect(self._show_about_window)
|
|
|
|
def update_display(self):
|
|
"""refresh translations of texts in main menubar when language change."""
|
|
self.file_Qmnu.setTitle(_("File"))
|
|
self.newWallet_Qact.setText(_("New Wallet"))
|
|
self.openWallet_Qact.setText(_("Open Wallet"))
|
|
self.filesHistory_Qmnu.setTitle(_("Last Files"))
|
|
self.noFileHistory_Qact.setText(_("No file found"))
|
|
self.hideApp_Qact.setText(_("Hide application"))
|
|
self.closeApp_Qact.setText(_("Close application"))
|
|
self.forceCloseApp_Qact.setText(_("Close without saving"))
|
|
self.langs_Qmnu.setTitle(_("Languages"))
|
|
self.help_Qmnu.setTitle(_("Help"))
|
|
self.help_Qact.setText(_("Help Content"))
|
|
self.about_Qact.setText(_("About %(app)s") % {"app":APP_NAME})
|
|
self.showApp_Qact.setText(_("Show application"))
|
|
self.main_Qtabw.setTabText(0, _("Tokens:"))
|
|
if self.wallet_Qw.wallet is None:
|
|
self.main_Qtabw.setTabText(1, _("Wallet: %(walletName)s") % {"walletName": ""})
|
|
else:
|
|
self.main_Qtabw.setTabText(1, _("Wallet: %(walletName)s") % {"walletName": self.wallet_Qw.wallet.walletName})
|
|
self.main_Qtabw.setTabText(2, _("Log Display:"))
|
|
self.wallet_Qw.update_display()
|
|
self.logViewer_Qw.update_display()
|
|
self.setLayoutDirection(get_qt_dir())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
logger.remove() # Supprime les sorties par défaut
|
|
logger.add(sys.stderr, level = 'TRACE', format = '<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{line: <4}</cyan> - <level>{message}</level>', filter = None, colorize = None, serialize = False, backtrace = True, diagnose = True, enqueue = False, context = None, catch = True)
|
|
logger.add("logs/KhadhroonyRaydium4.{time}.log", format = "{time:YYYY-MM-DD HH:mm:ss.SSS} - {level} - {line: <4}: {message}", level = "DEBUG")
|
|
app_lang = get_lang_or_fallback(get_system_language())
|
|
set_app_lang_code(app_lang)
|
|
gettext.bindtextdomain('argparse', 'locale')
|
|
gettext.textdomain('argparse')
|
|
parser = argparse.ArgumentParser(
|
|
description = "===~-<=<~~> No Khadhrawy <$<~>$> No Hamroony <~~>=>-~===\n\n" + APP_DESC + "\n" + _("Launch without options to show main window"),
|
|
formatter_class = argparse.RawTextHelpFormatter,
|
|
epilog = "===~-<=<~~> No Khadhrawy <$<~>$> No Hamroony <~~>=>-~==="
|
|
)
|
|
# parser.add_argument("-v", "--version", action="store_true", help="show version and exit.")
|
|
parser.add_argument('-v', '--version', action = 'version', version = "{prog} (v. {version})".format(prog = APP_NAME, version = APP_VERSION), help = _("show program version and exit"))
|
|
args = parser.parse_args()
|
|
# creation du répertoire de wallets
|
|
create_wallets_folder()
|
|
# chargement configuration de base / historique / langue / display etc ...
|
|
config = load_yaml_app_config()
|
|
configLocale = config["defaultLang"]
|
|
app_lang = get_lang_or_fallback(configLocale)
|
|
set_app_lang_code(app_lang)
|
|
config['defaultLang'] = app_lang
|
|
app = QApplication(sys.argv)
|
|
# chargement du SplashScreen
|
|
splash_screen = SplashScreen()
|
|
splash_screen.show()
|
|
# Planifier l'affichage de la fenêtre principale après le splash screen
|
|
main_window = MainWindow(config)
|
|
splash_screen.fadeOut.finished.connect(main_window.first_load)
|
|
|
|
sys.exit(app.exec())
|