Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion contrib/build-wine/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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/ && \
Expand Down
277 changes: 277 additions & 0 deletions electrum/migration.py
Original file line number Diff line number Diff line change
@@ -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:<br>"
f"<code>{electrum_dir}</code><br><br>"
f"Would you like to migrate it to:<br>"
f"<code>{factwallet_dir}</code><br><br>"
f"A backup will be created before migration<br><br>"
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)
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:
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, empty wallet."
)
# 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)
19 changes: 18 additions & 1 deletion electrum/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions electrum/version.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
9 changes: 7 additions & 2 deletions run_electrum
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -344,17 +345,21 @@ 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)
# note: The main script is often unpacked to a temporary directory from a bundled executable,
# 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)

Expand Down