Skip to content

Commit e682e7d

Browse files
committed
Merge #824: Migrate legacy wallets that are not loaded
8f2522d gui: Use menu for wallet migration (Ava Chow) d56a450 gui: Use wallet name for wallet migration rather than WalletModel (Ava Chow) c391858 gui: don't remove wallet manually before migration (furszy) bfba638 gui: Consolidate wallet display name to GUIUtil function (Ava Chow) 28fc562 wallet, interfaces: Include database format in listWalletDir (Ava Chow) Pull request description: Currently the Migrate Wallet menu item can only be used to migrate the currently loaded wallet. This is not suitable for the future when legacy wallets can no longer be loaded at all, but should still be able to be migrated. This PR changes that menu item into a menu list like Open Wallet and lets users migrate any legacy wallet in their wallet directory regardless of the wallets loaded. One issue I ran into was dealing with encrypted wallets. Ideally, we would detect whether a wallet is encrypted, and prompt the user for their passphrase at that time. However, that's actually difficult to do in the GUI since migration will unload the wallet if it was already loaded, and reload it without connecting it to any signals or interfaces. Only then can it detect whether a wallet is encrypted, but then there is no `WalletModel` or even an `interfaces::Wallet` that the GUI could use to unlock it via a callback. To deal with this, I've opted to just add a button to the migration dialog box that has the user enter their passphrase first, along with instructional text to use that button if their wallet was encrypted. If the user enters the wrong passphrase or clicked the other button that does not prompt for the passphrase, migration will fail with a message indicating that the passphrase was incorrect. ACKs for top commit: hebasto: ACK 8f2522d. furszy: ACK 8f2522d Tree-SHA512: a0e3b70dbfcacb89617956510ebcea94cad8617a987c68fe39fa16ac1721190b7cf7afc156c39b9032920cfb67b5d4ca28791681f5021d92d16acc691387afa1
2 parents 1a41e63 + 8f2522d commit e682e7d

15 files changed

+129
-56
lines changed

src/interfaces/wallet.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,11 @@ class WalletLoader : public ChainClient
342342
//! Migrate a wallet
343343
virtual util::Result<WalletMigrationResult> migrateWallet(const std::string& name, const SecureString& passphrase) = 0;
344344

345+
//! Returns true if wallet stores encryption keys
346+
virtual bool isEncrypted(const std::string& wallet_name) = 0;
347+
345348
//! Return available wallets in wallet directory.
346-
virtual std::vector<std::string> listWalletDir() = 0;
349+
virtual std::vector<std::pair<std::string, std::string>> listWalletDir() = 0;
347350

348351
//! Return interfaces for accessing wallets (if any).
349352
virtual std::vector<std::unique_ptr<Wallet>> getWallets() = 0;

src/qt/askpassphrasedialog.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ AskPassphraseDialog::AskPassphraseDialog(Mode _mode, QWidget *parent, SecureStri
4444
ui->passEdit1->hide();
4545
setWindowTitle(tr("Encrypt wallet"));
4646
break;
47+
case UnlockMigration:
4748
case Unlock: // Ask passphrase
4849
ui->warningLabel->setText(tr("This operation needs your wallet passphrase to unlock the wallet."));
4950
ui->passLabel2->hide();
@@ -80,7 +81,7 @@ void AskPassphraseDialog::setModel(WalletModel *_model)
8081
void AskPassphraseDialog::accept()
8182
{
8283
SecureString oldpass, newpass1, newpass2;
83-
if (!model && mode != Encrypt)
84+
if (!model && mode != Encrypt && mode != UnlockMigration)
8485
return;
8586
oldpass.reserve(MAX_PASSPHRASE_SIZE);
8687
newpass1.reserve(MAX_PASSPHRASE_SIZE);
@@ -181,6 +182,10 @@ void AskPassphraseDialog::accept()
181182
QMessageBox::critical(this, tr("Wallet unlock failed"), e.what());
182183
}
183184
break;
185+
case UnlockMigration:
186+
Assume(m_passphrase_out)->assign(oldpass);
187+
QDialog::accept();
188+
break;
184189
case ChangePass:
185190
if(newpass1 == newpass2)
186191
{
@@ -224,6 +229,7 @@ void AskPassphraseDialog::textChanged()
224229
case Encrypt: // New passphrase x2
225230
acceptable = !ui->passEdit2->text().isEmpty() && !ui->passEdit3->text().isEmpty();
226231
break;
232+
case UnlockMigration:
227233
case Unlock: // Old passphrase x1
228234
acceptable = !ui->passEdit1->text().isEmpty();
229235
break;

src/qt/askpassphrasedialog.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class AskPassphraseDialog : public QDialog
2626
Encrypt, /**< Ask passphrase twice and encrypt */
2727
Unlock, /**< Ask passphrase and unlock */
2828
ChangePass, /**< Ask old passphrase + new passphrase twice */
29+
UnlockMigration, /**< Ask passphrase for unlocking during migration */
2930
};
3031

3132
explicit AskPassphraseDialog(Mode mode, QWidget *parent, SecureString* passphrase_out = nullptr);

src/qt/bitcoingui.cpp

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ void BitcoinGUI::createActions()
360360
m_migrate_wallet_action = new QAction(tr("Migrate Wallet"), this);
361361
m_migrate_wallet_action->setEnabled(false);
362362
m_migrate_wallet_action->setStatusTip(tr("Migrate a wallet"));
363+
m_migrate_wallet_menu = new QMenu(this);
363364

364365
showHelpMessageAction = new QAction(tr("&Command-line options"), this);
365366
showHelpMessageAction->setMenuRole(QAction::NoRole);
@@ -396,15 +397,15 @@ void BitcoinGUI::createActions()
396397
connect(openAction, &QAction::triggered, this, &BitcoinGUI::openClicked);
397398
connect(m_open_wallet_menu, &QMenu::aboutToShow, [this] {
398399
m_open_wallet_menu->clear();
399-
for (const std::pair<const std::string, bool>& i : m_wallet_controller->listWalletDir()) {
400-
const std::string& path = i.first;
401-
QString name = path.empty() ? QString("["+tr("default wallet")+"]") : QString::fromStdString(path);
400+
for (const auto& [path, info] : m_wallet_controller->listWalletDir()) {
401+
const auto& [loaded, _] = info;
402+
QString name = GUIUtil::WalletDisplayName(path);
402403
// An single ampersand in the menu item's text sets a shortcut for this item.
403404
// Single & are shown when && is in the string. So replace & with &&.
404405
name.replace(QChar('&'), QString("&&"));
405406
QAction* action = m_open_wallet_menu->addAction(name);
406407

407-
if (i.second) {
408+
if (loaded) {
408409
// This wallet is already loaded
409410
action->setEnabled(false);
410411
continue;
@@ -455,10 +456,31 @@ void BitcoinGUI::createActions()
455456
connect(m_close_all_wallets_action, &QAction::triggered, [this] {
456457
m_wallet_controller->closeAllWallets(this);
457458
});
458-
connect(m_migrate_wallet_action, &QAction::triggered, [this] {
459-
auto activity = new MigrateWalletActivity(m_wallet_controller, this);
460-
connect(activity, &MigrateWalletActivity::migrated, this, &BitcoinGUI::setCurrentWallet);
461-
activity->migrate(walletFrame->currentWalletModel());
459+
connect(m_migrate_wallet_menu, &QMenu::aboutToShow, [this] {
460+
m_migrate_wallet_menu->clear();
461+
for (const auto& [wallet_name, info] : m_wallet_controller->listWalletDir()) {
462+
const auto& [loaded, format] = info;
463+
464+
if (format != "bdb") { // Skip already migrated wallets
465+
continue;
466+
}
467+
468+
QString name = GUIUtil::WalletDisplayName(wallet_name);
469+
// An single ampersand in the menu item's text sets a shortcut for this item.
470+
// Single & are shown when && is in the string. So replace & with &&.
471+
name.replace(QChar('&'), QString("&&"));
472+
QAction* action = m_migrate_wallet_menu->addAction(name);
473+
474+
connect(action, &QAction::triggered, [this, wallet_name] {
475+
auto activity = new MigrateWalletActivity(m_wallet_controller, this);
476+
connect(activity, &MigrateWalletActivity::migrated, this, &BitcoinGUI::setCurrentWallet);
477+
activity->migrate(wallet_name);
478+
});
479+
}
480+
if (m_migrate_wallet_menu->isEmpty()) {
481+
QAction* action = m_migrate_wallet_menu->addAction(tr("No wallets available"));
482+
action->setEnabled(false);
483+
}
462484
});
463485
connect(m_mask_values_action, &QAction::toggled, this, &BitcoinGUI::setPrivacy);
464486
connect(m_mask_values_action, &QAction::toggled, this, &BitcoinGUI::enableHistoryAction);
@@ -691,6 +713,8 @@ void BitcoinGUI::setWalletController(WalletController* wallet_controller, bool s
691713
m_open_wallet_action->setEnabled(true);
692714
m_open_wallet_action->setMenu(m_open_wallet_menu);
693715
m_restore_wallet_action->setEnabled(true);
716+
m_migrate_wallet_action->setEnabled(true);
717+
m_migrate_wallet_action->setMenu(m_migrate_wallet_menu);
694718

695719
GUIUtil::ExceptionSafeConnect(wallet_controller, &WalletController::walletAdded, this, &BitcoinGUI::addWallet);
696720
connect(wallet_controller, &WalletController::walletRemoved, this, &BitcoinGUI::removeWallet);
@@ -771,7 +795,6 @@ void BitcoinGUI::setCurrentWallet(WalletModel* wallet_model)
771795
}
772796
}
773797
updateWindowTitle();
774-
m_migrate_wallet_action->setEnabled(wallet_model->wallet().isLegacy());
775798
}
776799

777800
void BitcoinGUI::setCurrentWalletBySelectorIndex(int index)
@@ -805,7 +828,6 @@ void BitcoinGUI::setWalletActionsEnabled(bool enabled)
805828
openAction->setEnabled(enabled);
806829
m_close_wallet_action->setEnabled(enabled);
807830
m_close_all_wallets_action->setEnabled(enabled);
808-
m_migrate_wallet_action->setEnabled(enabled);
809831
}
810832

811833
void BitcoinGUI::createTrayIcon()

src/qt/guiutil.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,4 +1008,13 @@ void ShowModalDialogAsynchronously(QDialog* dialog)
10081008
dialog->show();
10091009
}
10101010

1011+
QString WalletDisplayName(const QString& name)
1012+
{
1013+
return name.isEmpty() ? "[" + QObject::tr("default wallet") + "]" : name;
1014+
}
1015+
1016+
QString WalletDisplayName(const std::string& name)
1017+
{
1018+
return WalletDisplayName(QString::fromStdString(name));
1019+
}
10111020
} // namespace GUIUtil

src/qt/guiutil.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,9 @@ namespace GUIUtil
436436
return false;
437437
}
438438

439+
QString WalletDisplayName(const std::string& name);
440+
QString WalletDisplayName(const QString& name);
441+
439442
} // namespace GUIUtil
440443

441444
#endif // BITCOIN_QT_GUIUTIL_H

src/qt/walletcontroller.cpp

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,16 @@ WalletController::~WalletController()
6565
delete m_activity_worker;
6666
}
6767

68-
std::map<std::string, bool> WalletController::listWalletDir() const
68+
std::map<std::string, std::pair<bool, std::string>> WalletController::listWalletDir() const
6969
{
7070
QMutexLocker locker(&m_mutex);
71-
std::map<std::string, bool> wallets;
72-
for (const std::string& name : m_node.walletLoader().listWalletDir()) {
73-
wallets[name] = false;
71+
std::map<std::string, std::pair<bool, std::string>> wallets;
72+
for (const auto& [name, format] : m_node.walletLoader().listWalletDir()) {
73+
wallets[name] = std::make_pair(false, format);
7474
}
7575
for (WalletModel* wallet_model : m_wallets) {
7676
auto it = wallets.find(wallet_model->wallet().getWalletName());
77-
if (it != wallets.end()) it->second = true;
77+
if (it != wallets.end()) it->second.first = true;
7878
}
7979
return wallets;
8080
}
@@ -150,9 +150,10 @@ WalletModel* WalletController::getOrCreateWallet(std::unique_ptr<interfaces::Wal
150150
assert(called);
151151

152152
connect(wallet_model, &WalletModel::unload, this, [this, wallet_model] {
153-
// Defer removeAndDeleteWallet when no modal widget is active.
153+
// Defer removeAndDeleteWallet when no modal widget is actively waiting for an action.
154154
// TODO: remove this workaround by removing usage of QDialog::exec.
155-
if (QApplication::activeModalWidget()) {
155+
QWidget* active_dialog = QApplication::activeModalWidget();
156+
if (active_dialog && dynamic_cast<QProgressDialog*>(active_dialog) == nullptr) {
156157
connect(qApp, &QApplication::focusWindowChanged, wallet_model, [this, wallet_model]() {
157158
if (!QApplication::activeModalWidget()) {
158159
removeAndDeleteWallet(wallet_model);
@@ -343,7 +344,7 @@ void OpenWalletActivity::finish()
343344

344345
void OpenWalletActivity::open(const std::string& path)
345346
{
346-
QString name = path.empty() ? QString("["+tr("default wallet")+"]") : QString::fromStdString(path);
347+
QString name = GUIUtil::WalletDisplayName(path);
347348

348349
showProgressDialog(
349350
//: Title of window indicating the progress of opening of a wallet.
@@ -436,12 +437,12 @@ void RestoreWalletActivity::finish()
436437
Q_EMIT finished();
437438
}
438439

439-
void MigrateWalletActivity::migrate(WalletModel* wallet_model)
440+
void MigrateWalletActivity::migrate(const std::string& name)
440441
{
441442
// Warn the user about migration
442443
QMessageBox box(m_parent_widget);
443444
box.setWindowTitle(tr("Migrate wallet"));
444-
box.setText(tr("Are you sure you wish to migrate the wallet <i>%1</i>?").arg(GUIUtil::HtmlEscape(wallet_model->getDisplayName())));
445+
box.setText(tr("Are you sure you wish to migrate the wallet <i>%1</i>?").arg(GUIUtil::HtmlEscape(GUIUtil::WalletDisplayName(name))));
445446
box.setInformativeText(tr("Migrating the wallet will convert this wallet to one or more descriptor wallets. A new wallet backup will need to be made.\n"
446447
"If this wallet contains any watchonly scripts, a new wallet will be created which contains those watchonly scripts.\n"
447448
"If this wallet contains any solvable but not watched scripts, a different and new wallet will be created which contains those scripts.\n\n"
@@ -452,31 +453,25 @@ void MigrateWalletActivity::migrate(WalletModel* wallet_model)
452453
box.setDefaultButton(QMessageBox::Yes);
453454
if (box.exec() != QMessageBox::Yes) return;
454455

455-
// Get the passphrase if it is encrypted regardless of it is locked or unlocked. We need the passphrase itself.
456456
SecureString passphrase;
457-
WalletModel::EncryptionStatus enc_status = wallet_model->getEncryptionStatus();
458-
if (enc_status == WalletModel::EncryptionStatus::Locked || enc_status == WalletModel::EncryptionStatus::Unlocked) {
459-
AskPassphraseDialog dlg(AskPassphraseDialog::Unlock, m_parent_widget, &passphrase);
460-
dlg.setModel(wallet_model);
461-
dlg.exec();
457+
if (node().walletLoader().isEncrypted(name)) {
458+
// Get the passphrase for the wallet
459+
AskPassphraseDialog dlg(AskPassphraseDialog::UnlockMigration, m_parent_widget, &passphrase);
460+
if (dlg.exec() == QDialog::Rejected) return;
462461
}
463462

464-
// GUI needs to remove the wallet so that it can actually be unloaded by migration
465-
const std::string name = wallet_model->wallet().getWalletName();
466-
m_wallet_controller->removeAndDeleteWallet(wallet_model);
467-
468463
showProgressDialog(tr("Migrate Wallet"), tr("Migrating Wallet <b>%1</b>…").arg(GUIUtil::HtmlEscape(name)));
469464

470465
QTimer::singleShot(0, worker(), [this, name, passphrase] {
471466
auto res{node().walletLoader().migrateWallet(name, passphrase)};
472467

473468
if (res) {
474-
m_success_message = tr("The wallet '%1' was migrated successfully.").arg(GUIUtil::HtmlEscape(res->wallet->getWalletName()));
469+
m_success_message = tr("The wallet '%1' was migrated successfully.").arg(GUIUtil::HtmlEscape(GUIUtil::WalletDisplayName(res->wallet->getWalletName())));
475470
if (res->watchonly_wallet_name) {
476-
m_success_message += QChar(' ') + tr("Watchonly scripts have been migrated to a new wallet named '%1'.").arg(GUIUtil::HtmlEscape(res->watchonly_wallet_name.value()));
471+
m_success_message += QChar(' ') + tr("Watchonly scripts have been migrated to a new wallet named '%1'.").arg(GUIUtil::HtmlEscape(GUIUtil::WalletDisplayName(res->watchonly_wallet_name.value())));
477472
}
478473
if (res->solvables_wallet_name) {
479-
m_success_message += QChar(' ') + tr("Solvable but not watched scripts have been migrated to a new wallet named '%1'.").arg(GUIUtil::HtmlEscape(res->solvables_wallet_name.value()));
474+
m_success_message += QChar(' ') + tr("Solvable but not watched scripts have been migrated to a new wallet named '%1'.").arg(GUIUtil::HtmlEscape(GUIUtil::WalletDisplayName(res->solvables_wallet_name.value())));
480475
}
481476
m_wallet_model = m_wallet_controller->getOrCreateWallet(std::move(res->wallet));
482477
} else {

src/qt/walletcontroller.h

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,11 @@ class WalletController : public QObject
6161

6262
//! Returns all wallet names in the wallet dir mapped to whether the wallet
6363
//! is loaded.
64-
std::map<std::string, bool> listWalletDir() const;
64+
std::map<std::string, std::pair<bool, std::string>> listWalletDir() const;
6565

6666
void closeWallet(WalletModel* wallet_model, QWidget* parent = nullptr);
6767
void closeAllWallets(QWidget* parent = nullptr);
6868

69-
void migrateWallet(WalletModel* wallet_model, QWidget* parent = nullptr);
70-
7169
Q_SIGNALS:
7270
void walletAdded(WalletModel* wallet_model);
7371
void walletRemoved(WalletModel* wallet_model);
@@ -186,7 +184,7 @@ class MigrateWalletActivity : public WalletControllerActivity
186184
public:
187185
MigrateWalletActivity(WalletController* wallet_controller, QWidget* parent) : WalletControllerActivity(wallet_controller, parent) {}
188186

189-
void migrate(WalletModel* wallet_model);
187+
void migrate(const std::string& path);
190188

191189
Q_SIGNALS:
192190
void migrated(WalletModel* wallet_model);

src/qt/walletmodel.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -594,8 +594,7 @@ QString WalletModel::getWalletName() const
594594

595595
QString WalletModel::getDisplayName() const
596596
{
597-
const QString name = getWalletName();
598-
return name.isEmpty() ? "["+tr("default wallet")+"]" : name;
597+
return GUIUtil::WalletDisplayName(getWalletName());
599598
}
600599

601600
bool WalletModel::isMultiwallet() const

src/wallet/db.cpp

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ namespace wallet {
1919
bool operator<(BytePrefix a, Span<const std::byte> b) { return a.prefix < b.subspan(0, std::min(a.prefix.size(), b.size())); }
2020
bool operator<(Span<const std::byte> a, BytePrefix b) { return a.subspan(0, std::min(a.size(), b.prefix.size())) < b.prefix; }
2121

22-
std::vector<fs::path> ListDatabases(const fs::path& wallet_dir)
22+
std::vector<std::pair<fs::path, std::string>> ListDatabases(const fs::path& wallet_dir)
2323
{
24-
std::vector<fs::path> paths;
24+
std::vector<std::pair<fs::path, std::string>> paths;
2525
std::error_code ec;
2626

2727
for (auto it = fs::recursive_directory_iterator(wallet_dir, ec); it != fs::recursive_directory_iterator(); it.increment(ec)) {
@@ -38,21 +38,29 @@ std::vector<fs::path> ListDatabases(const fs::path& wallet_dir)
3838
try {
3939
const fs::path path{it->path().lexically_relative(wallet_dir)};
4040

41-
if (it->status().type() == fs::file_type::directory &&
42-
(IsBDBFile(BDBDataFile(it->path())) || IsSQLiteFile(SQLiteDataFile(it->path())))) {
43-
// Found a directory which contains wallet.dat btree file, add it as a wallet.
44-
paths.emplace_back(path);
41+
if (it->status().type() == fs::file_type::directory) {
42+
if (IsBDBFile(BDBDataFile(it->path()))) {
43+
// Found a directory which contains wallet.dat btree file, add it as a wallet with BERKELEY format.
44+
paths.emplace_back(path, "bdb");
45+
} else if (IsSQLiteFile(SQLiteDataFile(it->path()))) {
46+
// Found a directory which contains wallet.dat sqlite file, add it as a wallet with SQLITE format.
47+
paths.emplace_back(path, "sqlite");
48+
}
4549
} else if (it.depth() == 0 && it->symlink_status().type() == fs::file_type::regular && it->path().extension() != ".bak") {
4650
if (it->path().filename() == "wallet.dat") {
47-
// Found top-level wallet.dat btree file, add top level directory ""
51+
// Found top-level wallet.dat file, add top level directory ""
4852
// as a wallet.
49-
paths.emplace_back();
53+
if (IsBDBFile(it->path())) {
54+
paths.emplace_back(fs::path(), "bdb");
55+
} else if (IsSQLiteFile(it->path())) {
56+
paths.emplace_back(fs::path(), "sqlite");
57+
}
5058
} else if (IsBDBFile(it->path())) {
5159
// Found top-level btree file not called wallet.dat. Current bitcoin
5260
// software will never create these files but will allow them to be
5361
// opened in a shared database environment for backwards compatibility.
5462
// Add it to the list of available wallets.
55-
paths.emplace_back(path);
63+
paths.emplace_back(path, "bdb");
5664
}
5765
}
5866
} catch (const std::exception& e) {

0 commit comments

Comments
 (0)