Skip to content

Commit 999540b

Browse files
Merge pull request #24 from NyanCatTW1/master
Switch to FactWallet-specific data directories and add UI for semi-auto migration
2 parents 2b726be + 32ad810 commit 999540b

5 files changed

Lines changed: 305 additions & 6 deletions

File tree

contrib/build-wine/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ RUN wget -nc https://dl.winehq.org/wine-builds/Release.key && \
4141
apt-key add Release.key && \
4242
rm Release.key && \
4343
wget -nc https://dl.winehq.org/wine-builds/winehq.key && \
44-
echo "78b185fabdb323971d13bd329fefc8038e08559aa51c4996de18db0639a51df6 winehq.key" | sha256sum -c - && \
44+
echo "d965d646defe94b3dfba6d5b4406900ac6c81065428bf9d9303ad7a72ee8d1b8 winehq.key" | sha256sum -c - && \
4545
apt-key add winehq.key && \
4646
rm winehq.key && \
4747
apt-add-repository https://dl.winehq.org/wine-builds/debian/ && \

electrum/migration.py

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
# Electrum - lightweight Bitcoin client
2+
# Copyright (C) 2011 Thomas Voegtlin
3+
#
4+
# Permission is hereby granted, free of charge, to any person
5+
# obtaining a copy of this software and associated documentation files
6+
# (the "Software"), to deal in the Software without restriction,
7+
# including without limitation the rights to use, copy, modify, merge,
8+
# publish, distribute, sublicense, and/or sell copies of the Software,
9+
# and to permit persons to whom the Software is furnished to do so,
10+
# subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be
13+
# included in all copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
19+
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
20+
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
# SOFTWARE.
23+
import os
24+
import json
25+
import shutil
26+
import datetime
27+
import stat
28+
29+
from PyQt5.QtWidgets import QMessageBox, QDialog, QVBoxLayout, QLabel, QPushButton, QApplication
30+
from PyQt5.QtCore import Qt
31+
32+
from .util import old_user_dir, user_dir, make_dir
33+
from .logging import get_logger
34+
35+
_logger = get_logger(__name__)
36+
37+
38+
def is_factwallet_data(directory: str) -> bool:
39+
"""
40+
Determine if a directory contains FactWallet data by checking for FACT genesis block hash.
41+
42+
Args:
43+
directory: Path to the suspected FactWallet data directory
44+
45+
Returns:
46+
bool: True if the directory contains FactWallet data
47+
"""
48+
if not os.path.exists(directory):
49+
return False
50+
51+
# Check config file for FACT genesis block hash
52+
config_path = os.path.join(directory, "config")
53+
if os.path.exists(config_path):
54+
try:
55+
with open(config_path, 'r', encoding='utf-8') as f:
56+
config = json.load(f)
57+
# Check for FACT genesis block hash
58+
blockchain_preferred_block = config.get('blockchain_preferred_block', {})
59+
block_hash = blockchain_preferred_block.get('hash')
60+
61+
# FACT genesis block hashes
62+
FACT_MAINNET_GENESIS = "79cb40f8075b0e3dc2bc468c5ce2a7acbe0afd36c6c3d3a134ea692edac7de49"
63+
FACT_TESTNET_GENESIS = "550bbf0a444d9f92189f067dd225f5b8a5d92587ebc2e8398d143236072580af"
64+
65+
if block_hash in [FACT_MAINNET_GENESIS, FACT_TESTNET_GENESIS]:
66+
return True
67+
68+
except (json.JSONDecodeError, OSError):
69+
pass
70+
71+
return False
72+
73+
74+
def perform_migration(electrum_dir: str, backup_dir: str, factwallet_dir: str) -> bool:
75+
"""
76+
Perform the migration: backup old dir and copy contents to new dir.
77+
78+
Args:
79+
electrum_dir: Source Electrum directory path
80+
backup_dir: Backup directory path
81+
factwallet_dir: Destination FactWallet directory path
82+
83+
Returns:
84+
bool: True if successful, False otherwise
85+
"""
86+
try:
87+
# 1. Move the old wallet directory to backup location
88+
_logger.info(f"Moving {electrum_dir} to {backup_dir}")
89+
shutil.move(electrum_dir, backup_dir)
90+
91+
# 2. Create the new FactWallet directory
92+
make_dir(factwallet_dir)
93+
94+
# 3. Copy all files from backup to new FactWallet directory (excluding socket files)
95+
_logger.info(f"Copying from {backup_dir} to {factwallet_dir}")
96+
def ignore_sockets(dir, files):
97+
socket_files = []
98+
for f in files:
99+
file_path = os.path.join(dir, f)
100+
try:
101+
if stat.S_ISSOCK(os.stat(file_path).st_mode):
102+
socket_files.append(f)
103+
except Exception:
104+
# Let files we can't identify pass
105+
pass
106+
return socket_files
107+
shutil.copytree(backup_dir, factwallet_dir, dirs_exist_ok=True, ignore=ignore_sockets)
108+
109+
# 4. Create a marker file to indicate successful migration
110+
marker_path = os.path.join(factwallet_dir, '.migrated_from_electrum')
111+
with open(marker_path, 'w', encoding='utf-8') as f:
112+
f.write(f"Migrated from `{electrum_dir}` (backup: `{backup_dir}`) to `{factwallet_dir}` on {datetime.datetime.now().isoformat()}")
113+
114+
return True
115+
except Exception as e:
116+
_logger.error(f"Migration failed: {e}")
117+
return False
118+
119+
120+
class MigrationDialog(QDialog):
121+
"""Dialog that asks the user if they want to migrate data from Electrum's directory to FactWallet's."""
122+
123+
def __init__(self, electrum_dir: str, factwallet_dir: str, parent=None):
124+
super().__init__(parent)
125+
self.electrum_dir = electrum_dir
126+
self.factwallet_dir = factwallet_dir
127+
when = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
128+
self.backup_dir = f"{electrum_dir}-backupByFactWallet-{when}"
129+
130+
self.result = False
131+
self.error = False
132+
133+
self.setWindowTitle("FactWallet Migration")
134+
self.setMinimumWidth(600)
135+
136+
layout = QVBoxLayout()
137+
138+
# Message
139+
message = QLabel(
140+
f"Your FACT0RN wallet is currently stored in:<br>"
141+
f"<code>{electrum_dir}</code><br><br>"
142+
f"Would you like to migrate it to:<br>"
143+
f"<code>{factwallet_dir}</code><br><br>"
144+
f"A backup will be created before migration<br><br>"
145+
f"If you choose No, a new, empty wallet will be created, and your old wallet will be left unchanged."
146+
)
147+
message.setWordWrap(True)
148+
message.setTextFormat(Qt.RichText)
149+
layout.addWidget(message)
150+
151+
# Buttons
152+
button_layout = QVBoxLayout()
153+
154+
self.migrate_button = QPushButton("Yes, migrate my wallet")
155+
self.migrate_button.clicked.connect(self.do_migrate)
156+
button_layout.addWidget(self.migrate_button)
157+
158+
self.skip_button = QPushButton("No, create a new wallet")
159+
self.skip_button.clicked.connect(self.reject)
160+
button_layout.addWidget(self.skip_button)
161+
162+
layout.addLayout(button_layout)
163+
self.setLayout(layout)
164+
165+
def do_migrate(self):
166+
# Perform the migration
167+
if perform_migration(self.electrum_dir, self.backup_dir, self.factwallet_dir):
168+
self.result = True
169+
# QMessageBox.information is a modal dialog - it will block execution until the user
170+
# acknowledges the message box by clicking OK. This ensures the migration dialog
171+
# doesn't complete its exec_() call until after the user sees and acknowledges the result.
172+
QMessageBox.information(
173+
self,
174+
"Migration Complete",
175+
f"Your wallet has been successfully migrated.\n\n"
176+
f"A backup containing your original wallet is available at:\n"
177+
f"{self.backup_dir}\n"
178+
)
179+
# This will only be called after the user acknowledges the info dialog
180+
self.accept()
181+
else:
182+
self.error = True
183+
QMessageBox.critical(
184+
self,
185+
"Migration Failed",
186+
f"Failed to migrate wallet from {self.electrum_dir} to {self.factwallet_dir}.\n\n"
187+
f"A backup containing your original wallet is available at:\n"
188+
f"{self.backup_dir}\n\n"
189+
f"Please try manually copying your wallet or create a new, empty wallet."
190+
)
191+
# This will only be called after the user acknowledges the critical dialog
192+
self.reject()
193+
194+
195+
def run_migration(electrum_dir: str, factwallet_dir: str) -> bool:
196+
"""
197+
Run the migration process after user confirmation.
198+
199+
Args:
200+
electrum_dir: Source Electrum directory path
201+
factwallet_dir: Destination FactWallet directory path
202+
203+
Returns:
204+
bool: True if migration was either successful or skipped by user
205+
"""
206+
# Initialize QApplication for the migration dialog
207+
app = QApplication.instance()
208+
if app is None:
209+
app = QApplication([])
210+
created_app = True
211+
else:
212+
created_app = False
213+
214+
try:
215+
# Show migration dialog
216+
dialog = MigrationDialog(electrum_dir, factwallet_dir)
217+
result = dialog.exec_() == QDialog.Accepted and dialog.result
218+
219+
if dialog.error:
220+
raise Exception("Error during wallet migration")
221+
222+
if result:
223+
_logger.info(f"Successfully migrated data from {electrum_dir} to {factwallet_dir}")
224+
else:
225+
_logger.info("User skipped migration")
226+
227+
return True
228+
except Exception as e:
229+
_logger.error(f"Error during migration: {e}")
230+
raise e
231+
finally:
232+
# Clean up the QApplication if we created it
233+
if created_app and app is not None:
234+
app.quit()
235+
236+
237+
def check_for_migration(config_options: dict = None) -> None:
238+
"""
239+
Check if migration is needed and handle it if so.
240+
241+
Args:
242+
config_options: Config options dictionary that gets passed to SimpleConfig
243+
"""
244+
is_portable = config_options.get('portable', False)
245+
246+
# Skip migration in these cases:
247+
# 1. On Android (each app has its own data directory)
248+
if 'ANDROID_DATA' in os.environ:
249+
_logger.info("Skipping migration check on Android")
250+
return
251+
252+
# 2. If user specified a custom data directory outside of portable mode
253+
if config_options and config_options.get('electrum_path') and not is_portable:
254+
_logger.info(f"Custom data directory specified: {config_options.get('electrum_path')}")
255+
return
256+
257+
# Locate data directories
258+
if is_portable:
259+
factwallet_dir = config_options['electrum_path']
260+
electrum_dir = os.path.join(os.path.dirname(config_options['electrum_path']), 'electrum_data')
261+
else:
262+
factwallet_dir = user_dir()
263+
electrum_dir = old_user_dir()
264+
265+
# Skip migration if FactWallet directory already exists
266+
if os.path.exists(factwallet_dir):
267+
_logger.info(f"FactWallet directory already exists: {factwallet_dir}")
268+
return
269+
270+
# Check if Electrum directory exists and contains FactWallet data
271+
if not electrum_dir or not os.path.exists(electrum_dir):
272+
_logger.info(f"No Electrum directory found at: {electrum_dir}")
273+
return
274+
275+
if is_factwallet_data(electrum_dir):
276+
_logger.info(f"Found FactWallet data in Electrum directory: {electrum_dir}")
277+
run_migration(electrum_dir, factwallet_dir)

electrum/util.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,8 @@ def xor_bytes(a: bytes, b: bytes) -> bytes:
624624
.to_bytes(size, "big"))
625625

626626

627-
def user_dir():
627+
# Returns the old user directory (for migration purposes)
628+
def old_user_dir():
628629
if "ELECTRUMDIR" in os.environ:
629630
return os.environ["ELECTRUMDIR"]
630631
elif 'ANDROID_DATA' in os.environ:
@@ -640,6 +641,22 @@ def user_dir():
640641
return
641642

642643

644+
def user_dir():
645+
if "FACTWALLETDIR" in os.environ:
646+
return os.environ["FACTWALLETDIR"]
647+
elif 'ANDROID_DATA' in os.environ:
648+
return android_data_dir() # No migration needed since it's per app
649+
elif os.name == 'posix':
650+
return os.path.join(os.environ["HOME"], ".factwallet")
651+
elif "APPDATA" in os.environ:
652+
return os.path.join(os.environ["APPDATA"], "FactWallet")
653+
elif "LOCALAPPDATA" in os.environ:
654+
return os.path.join(os.environ["LOCALAPPDATA"], "FactWallet")
655+
else:
656+
#raise Exception("No home directory found in environment variables.")
657+
return
658+
659+
643660
def resource_path(*parts):
644661
return os.path.join(pkg_dir, *parts)
645662

electrum/version.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
ELECTRUM_VERSION = '4.5' # version of the client package
2-
APK_VERSION = '4.4.6.0' # read by buildozer.spec
1+
ELECTRUM_VERSION = '4.6' # version of the client package
2+
APK_VERSION = '4.6.0.0' # read by buildozer.spec
33

44
PROTOCOL_VERSION = '1.4' # protocol version requested
55

run_electrum

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ from electrum import daemon
106106
from electrum import keystore
107107
from electrum.util import create_and_start_event_loop
108108
from electrum.i18n import set_language
109+
from electrum import migration
109110

110111
if TYPE_CHECKING:
111112
import threading
@@ -344,17 +345,21 @@ def main():
344345
if config_options.get('portable'):
345346
if is_local:
346347
# running from git clone or local source: put datadir next to main script
347-
datadir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'electrum_data')
348+
datadir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'factwallet_data')
348349
else:
349350
# Running a binary or installed source. The most generic but still reasonable thing
350351
# is to use the current working directory. (see #7732)
351352
# note: The main script is often unpacked to a temporary directory from a bundled executable,
352353
# and we don't want to put the datadir inside a temp dir.
353354
# note: Re the portable .exe on Windows, when the user double-clicks it, CWD gets set
354355
# to the parent dir, i.e. we will put the datadir next to the exe.
355-
datadir = os.path.join(os.path.realpath(cwd), 'electrum_data')
356+
datadir = os.path.join(os.path.realpath(cwd), 'factwallet_data')
356357
config_options['electrum_path'] = datadir
357358

359+
# Check if migration from Electrum to FactWallet data directory is needed
360+
# Only do this after -D/--dir and portable mode is handled and before a config is created
361+
migration.check_for_migration(config_options)
362+
358363
if not config_options.get('verbosity'):
359364
warnings.simplefilter('ignore', DeprecationWarning)
360365

0 commit comments

Comments
 (0)