From 59de7c4eea91ee8f813f5469a9a9aa2ebdd51e9a Mon Sep 17 00:00:00 2001 From: NyanCatTW1 <17372086+NyanCatTW1@users.noreply.github.com> Date: Sat, 26 Apr 2025 16:47:41 +0800 Subject: [PATCH 1/3] Update winehq.key to June 2024 checksum --- contrib/build-wine/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/build-wine/Dockerfile b/contrib/build-wine/Dockerfile index f394b6df0..e64622fb2 100644 --- a/contrib/build-wine/Dockerfile +++ b/contrib/build-wine/Dockerfile @@ -41,7 +41,7 @@ RUN wget -nc https://dl.winehq.org/wine-builds/Release.key && \ apt-key add Release.key && \ rm Release.key && \ wget -nc https://dl.winehq.org/wine-builds/winehq.key && \ - echo "78b185fabdb323971d13bd329fefc8038e08559aa51c4996de18db0639a51df6 winehq.key" | sha256sum -c - && \ + echo "d965d646defe94b3dfba6d5b4406900ac6c81065428bf9d9303ad7a72ee8d1b8 winehq.key" | sha256sum -c - && \ apt-key add winehq.key && \ rm winehq.key && \ apt-add-repository https://dl.winehq.org/wine-builds/debian/ && \ From 5fd0d19faee009d9f51cac8848f81af137e47908 Mon Sep 17 00:00:00 2001 From: NyanCatTW1 <17372086+NyanCatTW1@users.noreply.github.com> Date: Sat, 26 Apr 2025 17:03:49 +0800 Subject: [PATCH 2/3] Change data directory to be FactWallet-specific + add migration UI --- electrum/migration.py | 277 ++++++++++++++++++++++++++++++++++++++++++ electrum/util.py | 19 ++- electrum/version.py | 4 +- run_electrum | 9 +- 4 files changed, 304 insertions(+), 5 deletions(-) create mode 100644 electrum/migration.py diff --git a/electrum/migration.py b/electrum/migration.py new file mode 100644 index 000000000..a723c86a4 --- /dev/null +++ b/electrum/migration.py @@ -0,0 +1,277 @@ +# Electrum - lightweight Bitcoin client +# Copyright (C) 2011 Thomas Voegtlin +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +import os +import json +import shutil +import datetime +import stat + +from PyQt5.QtWidgets import QMessageBox, QDialog, QVBoxLayout, QLabel, QPushButton, QApplication +from PyQt5.QtCore import Qt + +from .util import old_user_dir, user_dir, make_dir +from .logging import get_logger + +_logger = get_logger(__name__) + + +def is_factwallet_data(directory: str) -> bool: + """ + Determine if a directory contains FactWallet data by checking for FACT genesis block hash. + + Args: + directory: Path to the suspected FactWallet data directory + + Returns: + bool: True if the directory contains FactWallet data + """ + if not os.path.exists(directory): + return False + + # Check config file for FACT genesis block hash + config_path = os.path.join(directory, "config") + if os.path.exists(config_path): + try: + with open(config_path, 'r', encoding='utf-8') as f: + config = json.load(f) + # Check for FACT genesis block hash + blockchain_preferred_block = config.get('blockchain_preferred_block', {}) + block_hash = blockchain_preferred_block.get('hash') + + # FACT genesis block hashes + FACT_MAINNET_GENESIS = "79cb40f8075b0e3dc2bc468c5ce2a7acbe0afd36c6c3d3a134ea692edac7de49" + FACT_TESTNET_GENESIS = "550bbf0a444d9f92189f067dd225f5b8a5d92587ebc2e8398d143236072580af" + + if block_hash in [FACT_MAINNET_GENESIS, FACT_TESTNET_GENESIS]: + return True + + except (json.JSONDecodeError, OSError): + pass + + return False + + +def perform_migration(electrum_dir: str, backup_dir: str, factwallet_dir: str) -> bool: + """ + Perform the migration: backup old dir and copy contents to new dir. + + Args: + electrum_dir: Source Electrum directory path + backup_dir: Backup directory path + factwallet_dir: Destination FactWallet directory path + + Returns: + bool: True if successful, False otherwise + """ + try: + # 1. Move the old wallet directory to backup location + _logger.info(f"Moving {electrum_dir} to {backup_dir}") + shutil.move(electrum_dir, backup_dir) + + # 2. Create the new FactWallet directory + make_dir(factwallet_dir) + + # 3. Copy all files from backup to new FactWallet directory (excluding socket files) + _logger.info(f"Copying from {backup_dir} to {factwallet_dir}") + def ignore_sockets(dir, files): + socket_files = [] + for f in files: + file_path = os.path.join(dir, f) + try: + if stat.S_ISSOCK(os.stat(file_path).st_mode): + socket_files.append(f) + except Exception: + # Let files we can't identify pass + pass + return socket_files + shutil.copytree(backup_dir, factwallet_dir, dirs_exist_ok=True, ignore=ignore_sockets) + + # 4. Create a marker file to indicate successful migration + marker_path = os.path.join(factwallet_dir, '.migrated_from_electrum') + with open(marker_path, 'w', encoding='utf-8') as f: + f.write(f"Migrated from `{electrum_dir}` (backup: `{backup_dir}`) to `{factwallet_dir}` on {datetime.datetime.now().isoformat()}") + + return True + except Exception as e: + _logger.error(f"Migration failed: {e}") + return False + + +class MigrationDialog(QDialog): + """Dialog that asks the user if they want to migrate data from Electrum's directory to FactWallet's.""" + + def __init__(self, electrum_dir: str, factwallet_dir: str, parent=None): + super().__init__(parent) + self.electrum_dir = electrum_dir + self.factwallet_dir = factwallet_dir + when = datetime.datetime.now().strftime("%Y%m%d%H%M%S") + self.backup_dir = f"{electrum_dir}-backupByFactWallet-{when}" + + self.result = False + self.error = False + + self.setWindowTitle("FactWallet Migration") + self.setMinimumWidth(600) + + layout = QVBoxLayout() + + # Message + message = QLabel( + f"Your FACT0RN wallet is currently stored in:
" + f"{electrum_dir}

" + f"Would you like to migrate it to:
" + f"{factwallet_dir}

" + f"A backup will be created before migration

" + f"If you choose to skip, a new wallet will be created." + ) + message.setWordWrap(True) + message.setTextFormat(Qt.RichText) + layout.addWidget(message) + + # Buttons + button_layout = QVBoxLayout() + + self.migrate_button = QPushButton("Yes, migrate my wallet") + self.migrate_button.clicked.connect(self.do_migrate) + button_layout.addWidget(self.migrate_button) + + self.skip_button = QPushButton("No, create a new wallet") + self.skip_button.clicked.connect(self.reject) + button_layout.addWidget(self.skip_button) + + layout.addLayout(button_layout) + self.setLayout(layout) + + def do_migrate(self): + # Perform the migration + if perform_migration(self.electrum_dir, self.backup_dir, self.factwallet_dir): + self.result = True + # QMessageBox.information is a modal dialog - it will block execution until the user + # acknowledges the message box by clicking OK. This ensures the migration dialog + # doesn't complete its exec_() call until after the user sees and acknowledges the result. + QMessageBox.information( + self, + "Migration Complete", + f"Your wallet has been successfully migrated.\n\n" + f"A backup containing your original wallet is available at:\n" + f"{self.backup_dir}\n" + ) + # This will only be called after the user acknowledges the info dialog + self.accept() + else: + QMessageBox.critical( + self, + "Migration Failed", + f"Failed to migrate wallet from {self.electrum_dir} to {self.factwallet_dir}.\n\n" + f"A backup containing your original wallet is available at:\n" + f"{self.backup_dir}\n\n" + f"Please try manually copying your wallet or create a new wallet." + ) + self.error = True + # This will only be called after the user acknowledges the critical dialog + self.reject() + + +def run_migration(electrum_dir: str, factwallet_dir: str) -> bool: + """ + Run the migration process after user confirmation. + + Args: + electrum_dir: Source Electrum directory path + factwallet_dir: Destination FactWallet directory path + + Returns: + bool: True if migration was either successful or skipped by user + """ + # Initialize QApplication for the migration dialog + app = QApplication.instance() + if app is None: + app = QApplication([]) + created_app = True + else: + created_app = False + + try: + # Show migration dialog + dialog = MigrationDialog(electrum_dir, factwallet_dir) + result = dialog.exec_() == QDialog.Accepted and dialog.result + + if dialog.error: + raise Exception("Error during wallet migration") + + if result: + _logger.info(f"Successfully migrated data from {electrum_dir} to {factwallet_dir}") + else: + _logger.info("User skipped migration") + + return True + except Exception as e: + _logger.error(f"Error during migration: {e}") + raise e + finally: + # Clean up the QApplication if we created it + if created_app and app is not None: + app.quit() + + +def check_for_migration(config_options: dict = None) -> None: + """ + Check if migration is needed and handle it if so. + + Args: + config_options: Config options dictionary that gets passed to SimpleConfig + """ + is_portable = config_options.get('portable', False) + + # Skip migration in these cases: + # 1. On Android (each app has its own data directory) + if 'ANDROID_DATA' in os.environ: + _logger.info("Skipping migration check on Android") + return + + # 2. If user specified a custom data directory outside of portable mode + if config_options and config_options.get('electrum_path') and not is_portable: + _logger.info(f"Custom data directory specified: {config_options.get('electrum_path')}") + return + + # Locate data directories + if is_portable: + factwallet_dir = config_options['electrum_path'] + electrum_dir = os.path.join(os.path.dirname(config_options['electrum_path']), 'electrum_data') + else: + factwallet_dir = user_dir() + electrum_dir = old_user_dir() + + # Skip migration if FactWallet directory already exists + if os.path.exists(factwallet_dir): + _logger.info(f"FactWallet directory already exists: {factwallet_dir}") + return + + # Check if Electrum directory exists and contains FactWallet data + if not electrum_dir or not os.path.exists(electrum_dir): + _logger.info(f"No Electrum directory found at: {electrum_dir}") + return + + if is_factwallet_data(electrum_dir): + _logger.info(f"Found FactWallet data in Electrum directory: {electrum_dir}") + run_migration(electrum_dir, factwallet_dir) diff --git a/electrum/util.py b/electrum/util.py index 4142a2ba2..6c5c5391a 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -624,7 +624,8 @@ def xor_bytes(a: bytes, b: bytes) -> bytes: .to_bytes(size, "big")) -def user_dir(): +# Returns the old user directory (for migration purposes) +def old_user_dir(): if "ELECTRUMDIR" in os.environ: return os.environ["ELECTRUMDIR"] elif 'ANDROID_DATA' in os.environ: @@ -640,6 +641,22 @@ def user_dir(): return +def user_dir(): + if "FACTWALLETDIR" in os.environ: + return os.environ["FACTWALLETDIR"] + elif 'ANDROID_DATA' in os.environ: + return android_data_dir() # No migration needed since it's per app + elif os.name == 'posix': + return os.path.join(os.environ["HOME"], ".factwallet") + elif "APPDATA" in os.environ: + return os.path.join(os.environ["APPDATA"], "FactWallet") + elif "LOCALAPPDATA" in os.environ: + return os.path.join(os.environ["LOCALAPPDATA"], "FactWallet") + else: + #raise Exception("No home directory found in environment variables.") + return + + def resource_path(*parts): return os.path.join(pkg_dir, *parts) diff --git a/electrum/version.py b/electrum/version.py index ad2f099e2..3581efb25 100644 --- a/electrum/version.py +++ b/electrum/version.py @@ -1,5 +1,5 @@ -ELECTRUM_VERSION = '4.5' # version of the client package -APK_VERSION = '4.4.6.0' # read by buildozer.spec +ELECTRUM_VERSION = '4.6' # version of the client package +APK_VERSION = '4.6.0.0' # read by buildozer.spec PROTOCOL_VERSION = '1.4' # protocol version requested diff --git a/run_electrum b/run_electrum index f1b612c55..1c11d0cc0 100755 --- a/run_electrum +++ b/run_electrum @@ -106,6 +106,7 @@ from electrum import daemon from electrum import keystore from electrum.util import create_and_start_event_loop from electrum.i18n import set_language +from electrum import migration if TYPE_CHECKING: import threading @@ -344,7 +345,7 @@ def main(): if config_options.get('portable'): if is_local: # running from git clone or local source: put datadir next to main script - datadir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'electrum_data') + datadir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'factwallet_data') else: # Running a binary or installed source. The most generic but still reasonable thing # is to use the current working directory. (see #7732) @@ -352,9 +353,13 @@ def main(): # and we don't want to put the datadir inside a temp dir. # note: Re the portable .exe on Windows, when the user double-clicks it, CWD gets set # to the parent dir, i.e. we will put the datadir next to the exe. - datadir = os.path.join(os.path.realpath(cwd), 'electrum_data') + datadir = os.path.join(os.path.realpath(cwd), 'factwallet_data') config_options['electrum_path'] = datadir + # Check if migration from Electrum to FactWallet data directory is needed + # Only do this after -D/--dir and portable mode is handled and before a config is created + migration.check_for_migration(config_options) + if not config_options.get('verbosity'): warnings.simplefilter('ignore', DeprecationWarning) From 32ad810a9b03c8d65c1a15ac37e1b44c57fba139 Mon Sep 17 00:00:00 2001 From: NyanCatTW1 <17372086+NyanCatTW1@users.noreply.github.com> Date: Sat, 26 Apr 2025 21:29:43 +0800 Subject: [PATCH 3/3] Clarify the consequence of not migrating the wallet --- electrum/migration.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/electrum/migration.py b/electrum/migration.py index a723c86a4..88c7ab10d 100644 --- a/electrum/migration.py +++ b/electrum/migration.py @@ -142,7 +142,7 @@ def __init__(self, electrum_dir: str, factwallet_dir: str, parent=None): f"Would you like to migrate it to:
" f"{factwallet_dir}

" f"A backup will be created before migration

" - f"If you choose to skip, a new wallet will be created." + f"If you choose No, a new, empty wallet will be created, and your old wallet will be left unchanged." ) message.setWordWrap(True) message.setTextFormat(Qt.RichText) @@ -179,15 +179,15 @@ def do_migrate(self): # This will only be called after the user acknowledges the info dialog self.accept() else: + self.error = True QMessageBox.critical( self, "Migration Failed", f"Failed to migrate wallet from {self.electrum_dir} to {self.factwallet_dir}.\n\n" f"A backup containing your original wallet is available at:\n" f"{self.backup_dir}\n\n" - f"Please try manually copying your wallet or create a new wallet." + f"Please try manually copying your wallet or create a new, empty wallet." ) - self.error = True # This will only be called after the user acknowledges the critical dialog self.reject()