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()