From f3219eef7d66e2e7a422435a4774dc01a1817a4e Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 5 Nov 2025 20:34:06 -0600 Subject: [PATCH 01/11] qt: avoid modifying QString during regex iteration in GUIUtil::loadStyleSheet Collect regex matches first, compute replacements using capturedStart/capturedEnd, then apply in reverse order to prevent iterator invalidation and QString corruption when processing sections. Also include for std::sort. --- src/qt/guiutil.cpp | 53 +++++++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp index dbf4e33c7821..9ae4964330f2 100644 --- a/src/qt/guiutil.cpp +++ b/src/qt/guiutil.cpp @@ -71,6 +71,7 @@ #include #include +#include #include #include #include @@ -958,7 +959,6 @@ void loadStyleSheet(bool fForceUpdate) QString strStyle = QLatin1String(qFile.readAll()); // Process all groups in the stylesheet first - QRegularExpressionMatch osStyleMatch; QRegularExpression osStyleExp( "^" "()" // group 1 @@ -966,29 +966,44 @@ void loadStyleSheet(bool fForceUpdate) "(?)" // group 3 "$"); osStyleExp.setPatternOptions(QRegularExpression::MultilineOption); - QRegularExpressionMatchIterator it = osStyleExp.globalMatch(strStyle); - // For all sections - while (it.hasNext() && (osStyleMatch = it.next()).isValid()) { - QStringList listMatches = osStyleMatch.capturedTexts(); - - // Full match + 3 group matches - if (listMatches.size() % 4) { - throw std::runtime_error(strprintf("%s: Invalid section in file %s", __func__, file.toStdString())); + // Collect matches first to avoid modifying the string while iterating + QList matches; + { + QRegularExpressionMatchIterator it = osStyleExp.globalMatch(strStyle); + while (it.hasNext()) { + QRegularExpressionMatch m = it.next(); + if (m.hasMatch()) { + matches.append(m); + } } + } - for (int i = 0; i < listMatches.size(); i += 4) { - if (!listMatches[i + 1].contains(QString::fromStdString(platformName))) { - // If os is not supported for this styles - // just remove the full match - strStyle.replace(listMatches[i], ""); - } else { - // If its supported remove the tags - strStyle.replace(listMatches[i + 1], ""); - strStyle.replace(listMatches[i + 3], ""); - } + // Build replacement operations using absolute positions + struct Replacement { int start; int end; QString replacement; }; + QVector replacements; + for (const auto& m : matches) { + const QString openTag = m.captured(1); + const QString inner = m.captured(2); + Q_UNUSED(inner); + // Remove entire block if OS doesn't match, otherwise drop only the tags + if (!openTag.contains(QString::fromStdString(platformName))) { + replacements.push_back({m.capturedStart(0), m.capturedEnd(0), QString()}); + } else { + // Remove opening and closing tags, keep inner content + replacements.push_back({m.capturedStart(1), m.capturedEnd(1), QString()}); + replacements.push_back({m.capturedStart(3), m.capturedEnd(3), QString()}); } } + + // Apply replacements from end to start so offsets stay valid + std::sort(replacements.begin(), replacements.end(), [](const Replacement& a, const Replacement& b) { + return a.start > b.start; + }); + for (const auto& r : replacements) { + strStyle.replace(r.start, r.end - r.start, r.replacement); + } + stylesheet->append(strStyle); } return true; From 77c3aeb128382ebff594f36aa32edd3ec5545cd4 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 5 Nov 2025 21:01:42 -0600 Subject: [PATCH 02/11] qt: Improve wallet creation UI flow - Add collapsible advanced options toggle in CreateWalletDialog - Add mnemonic verification dialog after wallet creation - Implement getMnemonic() interface method for retrieving mnemonic phrases - Add proper error handling for wallet lock state restoration - Add styling for both light and dark themes - Show verification dialog for HD wallets after creation - Skip verification for blank wallets and wallets with disabled private keys --- src/Makefile.qt.include | 4 + src/interfaces/wallet.h | 3 + src/qt/createwalletdialog.cpp | 12 + src/qt/forms/createwalletdialog.ui | 48 ++- src/qt/forms/mnemonicverificationdialog.ui | 206 ++++++++++ src/qt/mnemonicverificationdialog.cpp | 428 +++++++++++++++++++++ src/qt/mnemonicverificationdialog.h | 59 +++ src/qt/res/css/dark.css | 84 ++++ src/qt/res/css/light.css | 84 ++++ src/qt/walletcontroller.cpp | 105 ++++- src/qt/walletcontroller.h | 1 + src/wallet/interfaces.cpp | 53 +++ 12 files changed, 1085 insertions(+), 2 deletions(-) create mode 100644 src/qt/forms/mnemonicverificationdialog.ui create mode 100644 src/qt/mnemonicverificationdialog.cpp create mode 100644 src/qt/mnemonicverificationdialog.h diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index f50ac4602bfb..932b7a2f64ac 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -27,6 +27,7 @@ QT_FORMS_UI = \ qt/forms/intro.ui \ qt/forms/modaloverlay.ui \ qt/forms/masternodelist.ui \ + qt/forms/mnemonicverificationdialog.ui \ qt/forms/qrdialog.ui \ qt/forms/openuridialog.ui \ qt/forms/optionsdialog.ui \ @@ -66,6 +67,7 @@ QT_MOC_CPP = \ qt/moc_macnotificationhandler.cpp \ qt/moc_modaloverlay.cpp \ qt/moc_masternodelist.cpp \ + qt/moc_mnemonicverificationdialog.cpp \ qt/moc_notificator.cpp \ qt/moc_openuridialog.cpp \ qt/moc_optionsdialog.cpp \ @@ -144,6 +146,7 @@ BITCOIN_QT_H = \ qt/macos_appnap.h \ qt/modaloverlay.h \ qt/masternodelist.h \ + qt/mnemonicverificationdialog.h \ qt/networkstyle.h \ qt/notificator.h \ qt/openuridialog.h \ @@ -257,6 +260,7 @@ BITCOIN_QT_WALLET_CPP = \ qt/governancelist.cpp \ qt/proposalwizard.cpp \ qt/masternodelist.cpp \ + qt/mnemonicverificationdialog.cpp \ qt/openuridialog.cpp \ qt/overviewpage.cpp \ qt/paymentserver.cpp \ diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h index f704bd197ef7..56a2f595e666 100644 --- a/src/interfaces/wallet.h +++ b/src/interfaces/wallet.h @@ -297,6 +297,9 @@ class Wallet //! Return whether is a legacy wallet virtual bool isLegacy() = 0; + //! Get mnemonic phrase from wallet. + virtual bool getMnemonic(SecureString& mnemonic_out, SecureString& mnemonic_passphrase_out) = 0; + //! Register handler for unload message. using UnloadFn = std::function; virtual std::unique_ptr handleUnload(UnloadFn fn) = 0; diff --git a/src/qt/createwalletdialog.cpp b/src/qt/createwalletdialog.cpp index 5b7a7b353b4b..e10eacfa3d5e 100644 --- a/src/qt/createwalletdialog.cpp +++ b/src/qt/createwalletdialog.cpp @@ -12,6 +12,7 @@ #include #include +#include CreateWalletDialog::CreateWalletDialog(QWidget* parent) : QDialog(parent, GUIUtil::dialog_flags), @@ -22,6 +23,17 @@ CreateWalletDialog::CreateWalletDialog(QWidget* parent) : ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); ui->wallet_name_line_edit->setFocus(Qt::ActiveWindowFocusReason); + // Hide advanced options by default and provide a compact toggle control. + ui->groupBox->setVisible(false); + ui->groupBox->setTitle(QString()); + ui->advanced_toggle_button->setChecked(false); + ui->advanced_toggle_button->setArrowType(Qt::RightArrow); + ui->advanced_toggle_button->setFocusPolicy(Qt::NoFocus); + connect(ui->advanced_toggle_button, &QToolButton::toggled, this, [this](bool checked) { + ui->groupBox->setVisible(checked); + ui->advanced_toggle_button->setArrowType(checked ? Qt::DownArrow : Qt::RightArrow); + }); + connect(ui->wallet_name_line_edit, &QLineEdit::textEdited, [this](const QString& text) { ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!text.isEmpty()); }); diff --git a/src/qt/forms/createwalletdialog.ui b/src/qt/forms/createwalletdialog.ui index 49f66c25e418..26c93d861838 100644 --- a/src/qt/forms/createwalletdialog.ui +++ b/src/qt/forms/createwalletdialog.ui @@ -17,6 +17,9 @@ true + + 8 + @@ -71,11 +74,54 @@ + + + Advanced Options + + + Qt::ToolButtonTextBesideIcon + + + true + + + Qt::NoFocus + + + true + + + false + + + Qt::RightArrow + + + + - Advanced Options + + + + false + + 0 + + + 8 + + + 8 + + + 8 + + + 4 + diff --git a/src/qt/forms/mnemonicverificationdialog.ui b/src/qt/forms/mnemonicverificationdialog.ui new file mode 100644 index 000000000000..af07af8b3299 --- /dev/null +++ b/src/qt/forms/mnemonicverificationdialog.ui @@ -0,0 +1,206 @@ + + + MnemonicVerificationDialog + + + Save Your Mnemonic + + + + + + 0 + + + + + + + WARNING: If you lose your mnemonic seed phrase, you will lose access to your wallet forever. + + + Qt::RichText + + + true + + + + + + + Please write down these words in order. You will need them to restore your wallet. + + + true + + + + + + + QFrame::NoFrame + + + true + + + + + + + + + + + + Show + + + + + + + Hide + + + + + + + Qt::Horizontal + + + + 20 + 20 + + + + + + + + I have written down my mnemonic + + + + + + + + + + + + + To verify you've saved your mnemonic, please enter the following words: + + + true + + + + + + + + + Word #1: + + + + + + + + + + + + + + + + + Word #2: + + + + + + + + + + + + + + + + + Word #3: + + + + + + + + + + + + + + + + + + + + + Back + + + + + + + Qt::Horizontal + + + + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + MnemonicVerificationDialog + accept() + + + buttonBox + rejected() + MnemonicVerificationDialog + reject() + + + + diff --git a/src/qt/mnemonicverificationdialog.cpp b/src/qt/mnemonicverificationdialog.cpp new file mode 100644 index 000000000000..86a58e0e358c --- /dev/null +++ b/src/qt/mnemonicverificationdialog.cpp @@ -0,0 +1,428 @@ +// Copyright (c) 2024 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#if defined(HAVE_CONFIG_H) +#include +#endif + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +MnemonicVerificationDialog::MnemonicVerificationDialog(const SecureString& mnemonic, QWidget *parent) : + QDialog(parent, GUIUtil::dialog_flags), + ui(new Ui::MnemonicVerificationDialog), + m_mnemonic(mnemonic), + m_mnemonic_revealed(false) +{ + ui->setupUi(this); + + if (auto w = findChild("mnemonicGridWidget")) { + m_gridLayout = qobject_cast(w->layout()); + } + + // Keep minimum small so the page can compress when users scale down + setMinimumSize(QSize(550, 360)); + resize(minimumSize()); + + setWindowTitle(tr("Save Your Mnemonic")); + + // Words will be parsed on-demand to minimize exposure time in non-secure memory + // m_words is intentionally left empty initially + + // Trim outer paddings and inter-item spacing to avoid over-padded look + if (auto mainLayout = findChild("verticalLayout")) { + mainLayout->setContentsMargins(8, 4, 8, 6); + mainLayout->setSpacing(6); + } + if (auto s1 = findChild("verticalLayout_step1")) { + s1->setContentsMargins(8, 4, 8, 6); + s1->setSpacing(6); + } + if (auto s2 = findChild("verticalLayout_step2")) { + s2->setContentsMargins(8, 2, 8, 6); + s2->setSpacing(4); + s2->setAlignment(Qt::AlignTop); + } + if (ui->formLayout) { + ui->formLayout->setContentsMargins(0, 0, 0, 0); + ui->formLayout->setVerticalSpacing(3); + ui->formLayout->setHorizontalSpacing(8); + } + if (ui->buttonBox) { + ui->buttonBox->setContentsMargins(0, 0, 0, 0); + ui->buttonBox->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + } + + // Prefer compact default; we will adjust per-step to sizeHint + ui->stackedWidget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + // Ensure buttonBox is hidden initially (will be shown in step2) + ui->buttonBox->hide(); + setupStep1(); + adjustSize(); + m_defaultSize = size(); + + // Connections + connect(ui->showMnemonicButton, &QPushButton::clicked, this, &MnemonicVerificationDialog::onShowMnemonicClicked); + connect(ui->hideMnemonicButton, &QPushButton::clicked, this, &MnemonicVerificationDialog::onHideMnemonicClicked); + connect(ui->writtenDownCheckbox, &QCheckBox::toggled, this, [this](bool checked) { + if (checked && m_has_ever_revealed) setupStep2(); + }); + connect(ui->word1Edit, &QLineEdit::textChanged, this, &MnemonicVerificationDialog::onWord1Changed); + connect(ui->word2Edit, &QLineEdit::textChanged, this, &MnemonicVerificationDialog::onWord2Changed); + connect(ui->word3Edit, &QLineEdit::textChanged, this, &MnemonicVerificationDialog::onWord3Changed); + connect(ui->showMnemonicAgainButton, &QPushButton::clicked, this, &MnemonicVerificationDialog::onShowMnemonicAgainClicked); + + // Button box + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Continue")); + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &MnemonicVerificationDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &MnemonicVerificationDialog::reject); + + GUIUtil::handleCloseWindowShortcut(this); +} + +MnemonicVerificationDialog::~MnemonicVerificationDialog() +{ + clearWordsSecurely(); + clearMnemonic(); + delete ui; +} + +void MnemonicVerificationDialog::setupStep1() +{ + ui->stackedWidget->setCurrentIndex(0); + buildMnemonicGrid(false); + ui->hideMnemonicButton->hide(); + ui->showMnemonicButton->show(); + ui->writtenDownCheckbox->setEnabled(false); + ui->writtenDownCheckbox->setChecked(false); + m_mnemonic_revealed = false; + ui->buttonBox->hide(); + // Compact to content + adjustSize(); + + // Match visual hierarchy and tone of the improved mock + QString warningStyle = QString("font-size:17px; font-weight:700; ") + GUIUtil::getThemedStyleQString(GUIUtil::ThemedStyle::TS_ERROR); + QString instructionStyle = QString("font-size:14px; ") + GUIUtil::getThemedStyleQString(GUIUtil::ThemedStyle::TS_PRIMARY); + ui->warningLabel->setText( + tr("WARNING: If you lose your mnemonic seed phrase, you will lose access to your wallet forever. Write it down in a safe place and never share it with anyone.") + .arg(warningStyle) + ); + ui->instructionLabel->setText( + tr("Please write down these words in order. You will need them to restore your wallet.") + .arg(instructionStyle) + ); + + // Reduce extra padding to avoid an over-padded look + if (auto outer = findChild("verticalLayout_step1")) { + outer->setContentsMargins(12, 6, 12, 6); + outer->setSpacing(6); + } + if (auto hb = findChild("horizontalLayout_buttons")) { + hb->setContentsMargins(0, 4, 0, 0); + hb->setSpacing(10); + } +} + +void MnemonicVerificationDialog::setupStep2() +{ + ui->stackedWidget->setCurrentIndex(1); + // Parse words for validation (needed in step 2) + parseWords(); + generateRandomPositions(); + + ui->word1Edit->clear(); + ui->word2Edit->clear(); + ui->word3Edit->clear(); + ui->word1Edit->setMaximumWidth(320); + ui->word2Edit->setMaximumWidth(320); + ui->word3Edit->setMaximumWidth(320); + ui->word1Status->setMinimumWidth(18); + ui->word2Status->setMinimumWidth(18); + ui->word3Status->setMinimumWidth(18); + + ui->word1Label->setText(tr("Word #%1:").arg(m_selected_positions[0])); + ui->word2Label->setText(tr("Word #%1:").arg(m_selected_positions[1])); + ui->word3Label->setText(tr("Word #%1:").arg(m_selected_positions[2])); + + ui->word1Status->clear(); + ui->word2Status->clear(); + ui->word3Status->clear(); + + ui->buttonBox->show(); + if (QAbstractButton* cancel = ui->buttonBox->button(QDialogButtonBox::Cancel)) { + cancel->show(); + cancel->setText(tr("Back")); + disconnect(cancel, nullptr, nullptr, nullptr); + connect(cancel, &QAbstractButton::clicked, this, &MnemonicVerificationDialog::onShowMnemonicAgainClicked); + } + if (QAbstractButton* cont = ui->buttonBox->button(QDialogButtonBox::Ok)) cont->setEnabled(false); + if (ui->showMnemonicAgainButton) ui->showMnemonicAgainButton->hide(); + + // Ensure verification label has minimal top spacing + if (ui->verificationLabel) { + ui->verificationLabel->setStyleSheet("QLabel { margin-top: 0px; margin-bottom: 4px; }"); + } + + // Hide any existing title label if present + if (auto titleLabel = findChild("verifyTitleLabel")) { + titleLabel->hide(); + } + + // Align content toward top and remove any layout spacers expanding height FIRST + if (auto v = findChild("verticalLayout_step2")) { + v->setAlignment(Qt::AlignTop); + // Minimize top padding/margin to eliminate gap at top + QMargins m = v->contentsMargins(); + v->setContentsMargins(m.left(), 2, m.right(), m.bottom()); + for (int i = 0; i < v->count(); ++i) { + QLayoutItem* it = v->itemAt(i); + if (it && it->spacerItem()) it->spacerItem()->changeSize(0, 0, QSizePolicy::Fixed, QSizePolicy::Fixed); + } + // Force immediate layout update + v->invalidate(); + v->update(); + } + // Also ensure the main dialog layout has minimal top padding + if (auto mainLayout = findChild("verticalLayout")) { + QMargins m = mainLayout->contentsMargins(); + mainLayout->setContentsMargins(m.left(), 4, m.right(), m.bottom()); + mainLayout->invalidate(); + mainLayout->update(); + } + // Force widget to recalculate layout immediately + updateGeometry(); + + // Reduce minimums for verify; open exactly at the minimum size AFTER layout is fixed + setMinimumSize(QSize(460, 280)); + resize(minimumSize()); + adjustSize(); +} + +void MnemonicVerificationDialog::generateRandomPositions() +{ + m_selected_positions.clear(); + const int n = std::max(1, getWordCount()); + QSet used; + QRandomGenerator* rng = QRandomGenerator::global(); + while (m_selected_positions.size() < 3) { + int pos = rng->bounded(1, n + 1); + if (!used.contains(pos)) { used.insert(pos); m_selected_positions.append(pos); } + } + std::sort(m_selected_positions.begin(), m_selected_positions.end()); +} + +void MnemonicVerificationDialog::onShowMnemonicClicked() +{ + buildMnemonicGrid(true); + ui->showMnemonicButton->hide(); + ui->hideMnemonicButton->show(); + ui->writtenDownCheckbox->setEnabled(true); + m_mnemonic_revealed = true; + m_has_ever_revealed = true; +} + +void MnemonicVerificationDialog::onHideMnemonicClicked() +{ + buildMnemonicGrid(false); + ui->hideMnemonicButton->hide(); + ui->showMnemonicButton->show(); + m_mnemonic_revealed = false; + // Clear words from non-secure memory immediately when hiding + clearWordsSecurely(); +} + +void MnemonicVerificationDialog::onShowMnemonicAgainClicked() +{ + // Clear words when going back to step 1 (unless mnemonic is revealed) + if (!m_mnemonic_revealed) { + clearWordsSecurely(); + } + setupStep1(); +} + +void MnemonicVerificationDialog::onWord1Changed() { updateWordValidation(); } +void MnemonicVerificationDialog::onWord2Changed() { updateWordValidation(); } +void MnemonicVerificationDialog::onWord3Changed() { updateWordValidation(); } + +bool MnemonicVerificationDialog::validateWord(const QString& word, int position) +{ + // Parse words on-demand for validation (minimizes exposure time) + // Words are kept in memory during step 2 (verification) and step 1 (when revealed) + // They are only cleared when explicitly hiding in step 1 or on dialog destruction + QStringList words = parseWords(); + if (position < 1 || position > words.size()) { + return false; + } + return word == words[position - 1].toLower(); +} + +void MnemonicVerificationDialog::updateWordValidation() +{ + const QString t1 = ui->word1Edit->text().trimmed().toLower(); + const QString t2 = ui->word2Edit->text().trimmed().toLower(); + const QString t3 = ui->word3Edit->text().trimmed().toLower(); + + const bool ok1 = !t1.isEmpty() && validateWord(t1, m_selected_positions[0]); + const bool ok2 = !t2.isEmpty() && validateWord(t2, m_selected_positions[1]); + const bool ok3 = !t3.isEmpty() && validateWord(t3, m_selected_positions[2]); + + auto setStatus = [](QLabel* lbl, bool filled, bool valid) { + if (!lbl) return; + if (!filled) { lbl->clear(); lbl->setStyleSheet(""); return; } + if (valid) { + lbl->setText("✓"); + lbl->setStyleSheet(QString("QLabel { %1 font-weight: 700; }").arg(GUIUtil::getThemedStyleQString(GUIUtil::ThemedStyle::TS_SUCCESS))); + } else { + lbl->setText("✗"); + lbl->setStyleSheet(QString("QLabel { %1 font-weight: 700; }").arg(GUIUtil::getThemedStyleQString(GUIUtil::ThemedStyle::TS_ERROR))); + } + }; + setStatus(ui->word1Status, !t1.isEmpty(), ok1); + setStatus(ui->word2Status, !t2.isEmpty(), ok2); + setStatus(ui->word3Status, !t3.isEmpty(), ok3); + if (ui->buttonBox && ui->stackedWidget->currentIndex() == 1) { + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(ok1 && ok2 && ok3); + } +} + +void MnemonicVerificationDialog::accept() +{ + if (!validateWord(ui->word1Edit->text().trimmed().toLower(), m_selected_positions[0]) || + !validateWord(ui->word2Edit->text().trimmed().toLower(), m_selected_positions[1]) || + !validateWord(ui->word3Edit->text().trimmed().toLower(), m_selected_positions[2])) { + QMessageBox::warning(this, tr("Verification Failed"), tr("One or more words are incorrect. Please try again.")); + return; + } + QDialog::accept(); +} + +void MnemonicVerificationDialog::clearMnemonic() +{ + clearWordsSecurely(); + m_mnemonic.assign(m_mnemonic.size(), 0); +} + +QStringList MnemonicVerificationDialog::parseWords() +{ + // If words are already parsed, reuse them (for step 2 validation or step 1 display) + if (!m_words.isEmpty()) { + return m_words; + } + + // Parse words from secure mnemonic string + QString mnemonicStr = QString::fromStdString(std::string(m_mnemonic.begin(), m_mnemonic.end())); + m_words = mnemonicStr.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); + + // Clear the temporary QString immediately after parsing + mnemonicStr.clear(); + mnemonicStr.squeeze(); // Release memory + + return m_words; +} + +void MnemonicVerificationDialog::clearWordsSecurely() +{ + // Securely clear each word string by overwriting before clearing + for (QString& word : m_words) { + // Overwrite with zeros before clearing + word.fill(QChar(0)); + word.clear(); + word.squeeze(); // Release memory + } + m_words.clear(); +} + +int MnemonicVerificationDialog::getWordCount() const +{ + // Count words without parsing them into QStringList + // This avoids storing words in non-secure memory unnecessarily + if (m_words.isEmpty()) { + QString mnemonicStr = QString::fromStdString(std::string(m_mnemonic.begin(), m_mnemonic.end())); + QStringList words = mnemonicStr.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); + int count = words.size(); + // Clear immediately + mnemonicStr.clear(); + mnemonicStr.squeeze(); + return count; + } + return m_words.size(); +} + +void MnemonicVerificationDialog::buildMnemonicGrid(bool reveal) +{ + if (!m_gridLayout) return; + + QLayoutItem* child; + while ((child = m_gridLayout->takeAt(0)) != nullptr) { + if (child->widget()) child->widget()->deleteLater(); + delete child; + } + + // Parse words only when revealing (when needed for display) + QStringList words; + if (reveal) { + words = parseWords(); + } else { + // For hidden view, just get count without parsing words + const int n = getWordCount(); + const int columns = (n >= 24) ? 4 : 3; + const int rows = (n + columns - 1) / columns; + + QFont mono; mono.setStyleHint(QFont::Monospace); mono.setFamily("Monospace"); mono.setPointSize(13); + m_gridLayout->setContentsMargins(6, 2, 6, 8); + m_gridLayout->setHorizontalSpacing(32); + m_gridLayout->setVerticalSpacing(7); + + for (int r = 0; r < rows; ++r) { + for (int c = 0; c < columns; ++c) { + int idx = r * columns + c; if (idx >= n) break; + const QString text = QString("%1. •••••••").arg(idx + 1, 2); + QLabel* lbl = new QLabel(text); + lbl->setFont(mono); + lbl->setTextInteractionFlags(Qt::TextSelectableByMouse); + m_gridLayout->addWidget(lbl, r, c); + } + } + + m_gridLayout->setRowMinimumHeight(rows, 12); + return; + } + + // Revealed view - words are already parsed + const int n = words.size(); + const int columns = (n >= 24) ? 4 : 3; + const int rows = (n + columns - 1) / columns; + + QFont mono; mono.setStyleHint(QFont::Monospace); mono.setFamily("Monospace"); mono.setPointSize(13); + m_gridLayout->setContentsMargins(6, 2, 6, 8); + m_gridLayout->setHorizontalSpacing(32); + m_gridLayout->setVerticalSpacing(7); + + for (int r = 0; r < rows; ++r) { + for (int c = 0; c < columns; ++c) { + int idx = r * columns + c; if (idx >= n) break; + const QString text = QString("%1. %2").arg(idx + 1, 2).arg(words[idx]); + QLabel* lbl = new QLabel(text); + lbl->setFont(mono); + lbl->setTextInteractionFlags(Qt::TextSelectableByMouse); + m_gridLayout->addWidget(lbl, r, c); + } + } + + m_gridLayout->setRowMinimumHeight(rows, 12); +} + + diff --git a/src/qt/mnemonicverificationdialog.h b/src/qt/mnemonicverificationdialog.h new file mode 100644 index 000000000000..c58b7529a2de --- /dev/null +++ b/src/qt/mnemonicverificationdialog.h @@ -0,0 +1,59 @@ +// Copyright (c) 2024 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QT_MNEMONICVERIFICATIONDIALOG_H +#define BITCOIN_QT_MNEMONICVERIFICATIONDIALOG_H + +#include +#include + +#include + +namespace Ui { + class MnemonicVerificationDialog; +} + +/** Dialog to verify mnemonic seed phrase by asking user to enter 3 random word positions */ +class MnemonicVerificationDialog : public QDialog +{ + Q_OBJECT + +public: + explicit MnemonicVerificationDialog(const SecureString& mnemonic, QWidget *parent = nullptr); + ~MnemonicVerificationDialog(); + + void accept() override; + +private Q_SLOTS: + void onShowMnemonicClicked(); + void onHideMnemonicClicked(); + void onWord1Changed(); + void onWord2Changed(); + void onWord3Changed(); + void onShowMnemonicAgainClicked(); + +private: + void setupStep1(); + void setupStep2(); + void generateRandomPositions(); + void updateWordValidation(); + bool validateWord(const QString& word, int position); + void clearMnemonic(); + void buildMnemonicGrid(bool reveal); + QStringList parseWords(); + void clearWordsSecurely(); + int getWordCount() const; + + Ui::MnemonicVerificationDialog *ui; + SecureString m_mnemonic; + QStringList m_words; + QList m_selected_positions; + bool m_mnemonic_revealed; + bool m_has_ever_revealed{false}; + class QGridLayout* m_gridLayout{nullptr}; + QSize m_defaultSize; +}; + +#endif // BITCOIN_QT_MNEMONICVERIFICATIONDIALOG_H + diff --git a/src/qt/res/css/dark.css b/src/qt/res/css/dark.css index 0a534419fcff..9fe5b09762ed 100644 --- a/src/qt/res/css/dark.css +++ b/src/qt/res/css/dark.css @@ -1088,4 +1088,88 @@ QScrollBar:right-arrow:disabled { image: url(':/images/arrow_light_right_hover'); } +/** + * CreateWalletDialog (Dark Theme) + */ + +QDialog#CreateWalletDialog QLabel, +QDialog#CreateWalletDialog QCheckBox { + color: #bbbbbb; /* slightly lighter labels */ +} + +QDialog#CreateWalletDialog QToolButton#advanced_toggle_button { + color: #bdbdbd !important; /* lighter chevron/text */ +} + +QDialog#CreateWalletDialog QToolButton#advanced_toggle_button:hover { + color: #e0e0e0 !important; /* lighter on hover for better feedback */ +} + +QDialog#CreateWalletDialog QGroupBox#groupBox { + background-color: #2a2a2a; /* card background */ + border: 1px solid #3c3c3c; /* subtle outline */ + border-radius: 8px; + padding: 2px 12px 12px 12px; /* minimal top padding */ + margin-top: 0px; /* remove extra gap above card */ +} + +QDialog#CreateWalletDialog QGroupBox#groupBox::title { + padding: 0px; /* ensure no extra space for (empty) title */ +} + +QDialog#CreateWalletDialog QLineEdit:focus { + border-color: #4da3ff; /* subtle macOS-like blue */ +} + +/** + * MnemonicVerificationDialog (Dark Theme) + */ + +QDialog#MnemonicVerificationDialog QLabel { + color: #c7c7c7; /* light text for dark theme */ +} + +QDialog#MnemonicVerificationDialog QCheckBox { + color: #c7c7c7; /* light text for checkbox */ +} + +QDialog#MnemonicVerificationDialog QCheckBox#writtenDownCheckbox { + color: #c7c7c7; /* ensure checkbox text is visible */ +} + +QDialog#MnemonicVerificationDialog QScrollArea#mnemonicScroll { + background-color: #2d2d2e !important; /* dark background for scroll area */ + border: 1px solid #3c3c3c !important; /* subtle border */ + border-radius: 8px; +} + +QDialog#MnemonicVerificationDialog QScrollArea#mnemonicScroll QWidget#mnemonicGridWidget { + background-color: #2d2d2e !important; /* dark background matching scroll area */ +} + +QDialog#MnemonicVerificationDialog QWidget#mnemonicGridWidget { + background-color: #2d2d2e !important; /* dark background matching scroll area */ +} + +QDialog#MnemonicVerificationDialog QScrollArea#mnemonicScroll QWidget#mnemonicGridWidget QLabel { + color: #c7c7c7 !important; /* ensure grid labels are visible */ + background-color: transparent !important; +} + +QDialog#MnemonicVerificationDialog QLineEdit { + background-color: #2d2d2e; /* dark background for input fields */ + border-color: #00599a; + color: #c7c7c7; +} + +QDialog#MnemonicVerificationDialog QLineEdit:focus { + border-color: #4da3ff; /* subtle macOS-like blue */ +} + +QDialog#MnemonicVerificationDialog QLabel#word1Status, +QDialog#MnemonicVerificationDialog QLabel#word2Status, +QDialog#MnemonicVerificationDialog QLabel#word3Status { + color: #c7c7c7; /* ensure status labels are visible */ +} + diff --git a/src/qt/res/css/light.css b/src/qt/res/css/light.css index 7d15950351bc..4657ff8e9902 100644 --- a/src/qt/res/css/light.css +++ b/src/qt/res/css/light.css @@ -1072,4 +1072,88 @@ QScrollBar:right-arrow:disabled { image: url(':/images/arrow_light_right_normal'); } +/** + * CreateWalletDialog (Light Theme) + */ + +QDialog#CreateWalletDialog QLabel, +QDialog#CreateWalletDialog QCheckBox { + color: #555; /* ensure contrast consistent with labels */ +} + +QDialog#CreateWalletDialog QToolButton#advanced_toggle_button { + color: #555 !important; /* ensure good contrast in light mode */ +} + +QDialog#CreateWalletDialog QToolButton#advanced_toggle_button:hover { + color: #333 !important; /* darker on hover for better feedback */ +} + +QDialog#CreateWalletDialog QGroupBox#groupBox { + background-color: #eaeaec; /* card background */ + border: 1px solid #dcdcdc; /* subtle outline */ + border-radius: 8px; + padding: 2px 12px 12px 12px; /* minimal top padding */ + margin-top: 0px; /* remove extra gap above card */ +} + +QDialog#CreateWalletDialog QGroupBox#groupBox::title { + padding: 0px; /* ensure no extra space for (empty) title */ +} + +QDialog#CreateWalletDialog QLineEdit:focus { + border-color: #4da3ff; /* macOS-like blue */ +} + +/** + * MnemonicVerificationDialog (Light Theme) + */ + +QDialog#MnemonicVerificationDialog QLabel { + color: #555; /* ensure good contrast for all labels */ +} + +QDialog#MnemonicVerificationDialog QCheckBox { + color: #555; /* ensure checkbox text is visible */ +} + +QDialog#MnemonicVerificationDialog QCheckBox#writtenDownCheckbox { + color: #555; /* ensure checkbox text has good contrast */ +} + +QDialog#MnemonicVerificationDialog QScrollArea#mnemonicScroll { + background-color: #eaeaec !important; /* light background for scroll area */ + border: 1px solid #dcdcdc !important; /* subtle border */ + border-radius: 8px; +} + +QDialog#MnemonicVerificationDialog QScrollArea#mnemonicScroll QWidget#mnemonicGridWidget { + background-color: #eaeaec !important; /* light background matching scroll area */ +} + +QDialog#MnemonicVerificationDialog QWidget#mnemonicGridWidget { + background-color: #eaeaec !important; /* light background matching scroll area */ +} + +QDialog#MnemonicVerificationDialog QScrollArea#mnemonicScroll QWidget#mnemonicGridWidget QLabel { + color: #555 !important; /* ensure grid labels are visible */ + background-color: transparent !important; +} + +QDialog#MnemonicVerificationDialog QLineEdit { + background-color: #ffffff; /* white background for input fields */ + border-color: #008de4; + color: #555; +} + +QDialog#MnemonicVerificationDialog QLineEdit:focus { + border-color: #4da3ff; /* macOS-like blue */ +} + +QDialog#MnemonicVerificationDialog QLabel#word1Status, +QDialog#MnemonicVerificationDialog QLabel#word2Status, +QDialog#MnemonicVerificationDialog QLabel#word3Status { + color: #555; /* ensure status labels are visible */ +} + diff --git a/src/qt/walletcontroller.cpp b/src/qt/walletcontroller.cpp index eacb3ffc4832..5c71970cf20c 100644 --- a/src/qt/walletcontroller.cpp +++ b/src/qt/walletcontroller.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -279,11 +280,113 @@ void CreateWalletActivity::finish() QMessageBox::warning(m_parent_widget, tr("Create wallet warning"), QString::fromStdString(Join(m_warning_message, Untranslated("\n")).translated)); } - if (m_wallet_model) Q_EMIT created(m_wallet_model); + if (m_wallet_model) { + // Check if wallet is HD-enabled (has mnemonic) and requires verification + // Skip verification for blank wallets or wallets with disabled private keys + if (!m_wallet_model->wallet().hdEnabled() || + m_wallet_model->wallet().privateKeysDisabled() || + !m_wallet_model->wallet().canGetAddresses()) { + // Not an HD wallet - skip verification + Q_EMIT created(m_wallet_model); + Q_EMIT finished(); + return; + } + + // Unlock wallet if encrypted (needed to retrieve mnemonic) + bool was_locked = false; + bool was_unlocked_for_mixing = false; + WalletModel::EncryptionStatus enc_status = m_wallet_model->getEncryptionStatus(); + if (enc_status == WalletModel::Locked || enc_status == WalletModel::UnlockedForMixingOnly) { + was_locked = (enc_status == WalletModel::Locked); + was_unlocked_for_mixing = (enc_status == WalletModel::UnlockedForMixingOnly); + // Unlock fully (not just for mixing) to retrieve mnemonic + if (!m_wallet_model->setWalletLocked(false, m_passphrase, false)) { + QMessageBox::warning(m_parent_widget, tr("Unlock failed"), + tr("Failed to unlock wallet for mnemonic verification. Wallet creation completed but verification skipped.")); + Q_EMIT created(m_wallet_model); + Q_EMIT finished(); + return; + } + } + + // Check if wallet has a mnemonic and requires verification + SecureString mnemonic; + SecureString mnemonic_passphrase; + bool has_mnemonic = m_wallet_model->wallet().getMnemonic(mnemonic, mnemonic_passphrase); + + if (!has_mnemonic || mnemonic.empty()) { + // No mnemonic found - log warning and skip verification + restoreWalletLockState(was_locked, was_unlocked_for_mixing); + // Clear sensitive data before showing message + mnemonic.assign(mnemonic.size(), 0); + mnemonic_passphrase.assign(mnemonic_passphrase.size(), 0); + QMessageBox::warning(m_parent_widget, tr("Mnemonic retrieval failed"), + tr("Could not retrieve mnemonic phrase from wallet. Wallet creation completed but verification skipped.")); + Q_EMIT created(m_wallet_model); + Q_EMIT finished(); + return; + } + + // Wallet has mnemonic - show verification dialog + MnemonicVerificationDialog verify_dialog(mnemonic, m_parent_widget); + verify_dialog.setWindowModality(Qt::ApplicationModal); + + // Clear mnemonic from local variables after dialog has copied it + // The dialog will manage its own copy securely + const size_t mnemonic_size = mnemonic.size(); + const size_t passphrase_size = mnemonic_passphrase.size(); + mnemonic.assign(mnemonic_size, 0); + mnemonic_passphrase.assign(passphrase_size, 0); + + if (verify_dialog.exec() == QDialog::Accepted) { + // Verification successful + restoreWalletLockState(was_locked, was_unlocked_for_mixing); + Q_EMIT created(m_wallet_model); + } else { + // User cancelled verification + restoreWalletLockState(was_locked, was_unlocked_for_mixing); + QMessageBox::warning(m_parent_widget, tr("Verification cancelled"), + tr("You cancelled mnemonic verification. Please make sure you have saved your mnemonic phrase safely.")); + Q_EMIT created(m_wallet_model); + } + } else { + // Wallet creation failed - no wallet model + // Already showed error message above + } Q_EMIT finished(); } +void CreateWalletActivity::restoreWalletLockState(bool was_locked, bool was_unlocked_for_mixing) +{ + if (!was_locked && !was_unlocked_for_mixing) { + return; // Wallet was not locked, nothing to restore + } + + if (!m_wallet_model) { + return; // Safety check: wallet model not available + } + + if (was_unlocked_for_mixing) { + // Restore previous mixing-only unlock state + if (!m_wallet_model->setWalletLocked(true)) { + QMessageBox::warning(m_parent_widget, tr("Warning"), + tr("Failed to lock wallet. Please lock it manually.")); + return; + } + if (!m_wallet_model->setWalletLocked(false, m_passphrase, true)) { + QMessageBox::warning(m_parent_widget, tr("Warning"), + tr("Failed to restore mixing unlock state.")); + } + } else { + // Restore fully locked state + if (!m_wallet_model->setWalletLocked(true)) { + QMessageBox::warning(m_parent_widget, tr("Warning"), + tr("Failed to lock wallet. Please lock it manually.")); + } + } +} + void CreateWalletActivity::create() { m_create_wallet_dialog = new CreateWalletDialog(m_parent_widget); diff --git a/src/qt/walletcontroller.h b/src/qt/walletcontroller.h index ccb7dbc62d51..6d269eda0afb 100644 --- a/src/qt/walletcontroller.h +++ b/src/qt/walletcontroller.h @@ -123,6 +123,7 @@ class CreateWalletActivity : public WalletControllerActivity void askPassphrase(); void createWallet(); void finish(); + void restoreWalletLockState(bool was_locked, bool was_unlocked_for_mixing); SecureString m_passphrase; CreateWalletDialog* m_create_wallet_dialog{nullptr}; diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index 4c134c76220e..a64fc66dd3f8 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -30,6 +30,8 @@ #include #include #include +#include +#include #include #include #include @@ -523,6 +525,57 @@ class WalletImpl : public Wallet RemoveWallet(m_context, m_wallet, false /* load_on_start */); } bool isLegacy() override { return m_wallet->IsLegacy(); } + bool getMnemonic(SecureString& mnemonic_out, SecureString& mnemonic_passphrase_out) override + { + LOCK(m_wallet->cs_wallet); + + mnemonic_out.clear(); + mnemonic_passphrase_out.clear(); + + if (m_wallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + return false; + } + + if (m_wallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) { + // Descriptor wallet + for (auto spk_man : m_wallet->GetActiveScriptPubKeyMans()) { + if (auto desc_spk_man = dynamic_cast(spk_man)) { + if (desc_spk_man->GetMnemonicString(mnemonic_out, mnemonic_passphrase_out)) { + return true; + } + } + } + return false; + } else { + // Legacy wallet + auto spk_man = m_wallet->GetLegacyScriptPubKeyMan(); + if (!spk_man) { + return false; + } + + CHDChain hdChainCurrent; + if (!spk_man->GetHDChain(hdChainCurrent)) { + return false; + } + + // Get decrypted HD chain if wallet is encrypted + if (m_wallet->IsCrypted()) { + if (!spk_man->GetDecryptedHDChain(hdChainCurrent)) { + return false; + } + } + + SecureString ssMnemonic; + SecureString ssMnemonicPassphrase; + if (!hdChainCurrent.GetMnemonic(ssMnemonic, ssMnemonicPassphrase)) { + return false; + } + + mnemonic_out = ssMnemonic; + mnemonic_passphrase_out = ssMnemonicPassphrase; + return true; + } + } std::unique_ptr handleUnload(UnloadFn fn) override { return MakeHandler(m_wallet->NotifyUnload.connect(fn)); From e08e29f32b8a6ba2e57065b05db23e8661524096 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 5 Nov 2025 22:08:49 -0600 Subject: [PATCH 03/11] qt: Fix potential infinite loop and missing include in mnemonic verification - Guard against short mnemonics (< 3 words) to prevent infinite loop - Add validation in setupStep2() to check word count before proceeding - Add safety check after generateRandomPositions() to ensure positions were generated - Add missing #include for std::sort usage --- src/qt/mnemonicverificationdialog.cpp | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/qt/mnemonicverificationdialog.cpp b/src/qt/mnemonicverificationdialog.cpp index 86a58e0e358c..10d2068284fa 100644 --- a/src/qt/mnemonicverificationdialog.cpp +++ b/src/qt/mnemonicverificationdialog.cpp @@ -21,6 +21,8 @@ #include #include +#include + MnemonicVerificationDialog::MnemonicVerificationDialog(const SecureString& mnemonic, QWidget *parent) : QDialog(parent, GUIUtil::dialog_flags), ui(new Ui::MnemonicVerificationDialog), @@ -141,7 +143,25 @@ void MnemonicVerificationDialog::setupStep2() ui->stackedWidget->setCurrentIndex(1); // Parse words for validation (needed in step 2) parseWords(); + + // Validate mnemonic has at least 3 words before proceeding + const int wordCount = getWordCount(); + if (wordCount < 3) { + QMessageBox::critical(this, tr("Invalid Mnemonic"), + tr("Mnemonic phrase has fewer than 3 words (found %1). Verification cannot proceed.").arg(wordCount)); + reject(); + return; + } + generateRandomPositions(); + + // Safety check: ensure positions were generated successfully + if (m_selected_positions.size() < 3) { + QMessageBox::critical(this, tr("Verification Error"), + tr("Failed to generate verification positions. Please try again.")); + reject(); + return; + } ui->word1Edit->clear(); ui->word2Edit->clear(); @@ -214,7 +234,11 @@ void MnemonicVerificationDialog::setupStep2() void MnemonicVerificationDialog::generateRandomPositions() { m_selected_positions.clear(); - const int n = std::max(1, getWordCount()); + const int n = getWordCount(); + if (n < 3) { + // Unable to verify; bail out so the dialog can surface an error message upstream. + return; + } QSet used; QRandomGenerator* rng = QRandomGenerator::global(); while (m_selected_positions.size() < 3) { From 9894f47c3478ab4b156214e4baca8aab11efb21d Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 5 Nov 2025 22:21:47 -0600 Subject: [PATCH 04/11] Fix trailing whitespace in wallet creation files --- src/qt/mnemonicverificationdialog.cpp | 14 +++++++------- src/qt/walletcontroller.cpp | 20 ++++++++++---------- src/wallet/interfaces.cpp | 14 +++++++------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/qt/mnemonicverificationdialog.cpp b/src/qt/mnemonicverificationdialog.cpp index 10d2068284fa..418f72c3844f 100644 --- a/src/qt/mnemonicverificationdialog.cpp +++ b/src/qt/mnemonicverificationdialog.cpp @@ -143,7 +143,7 @@ void MnemonicVerificationDialog::setupStep2() ui->stackedWidget->setCurrentIndex(1); // Parse words for validation (needed in step 2) parseWords(); - + // Validate mnemonic has at least 3 words before proceeding const int wordCount = getWordCount(); if (wordCount < 3) { @@ -152,9 +152,9 @@ void MnemonicVerificationDialog::setupStep2() reject(); return; } - + generateRandomPositions(); - + // Safety check: ensure positions were generated successfully if (m_selected_positions.size() < 3) { QMessageBox::critical(this, tr("Verification Error"), @@ -195,7 +195,7 @@ void MnemonicVerificationDialog::setupStep2() if (ui->verificationLabel) { ui->verificationLabel->setStyleSheet("QLabel { margin-top: 0px; margin-bottom: 4px; }"); } - + // Hide any existing title label if present if (auto titleLabel = findChild("verifyTitleLabel")) { titleLabel->hide(); @@ -345,15 +345,15 @@ QStringList MnemonicVerificationDialog::parseWords() if (!m_words.isEmpty()) { return m_words; } - + // Parse words from secure mnemonic string QString mnemonicStr = QString::fromStdString(std::string(m_mnemonic.begin(), m_mnemonic.end())); m_words = mnemonicStr.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); - + // Clear the temporary QString immediately after parsing mnemonicStr.clear(); mnemonicStr.squeeze(); // Release memory - + return m_words; } diff --git a/src/qt/walletcontroller.cpp b/src/qt/walletcontroller.cpp index 5c71970cf20c..0a1e194d13a9 100644 --- a/src/qt/walletcontroller.cpp +++ b/src/qt/walletcontroller.cpp @@ -283,7 +283,7 @@ void CreateWalletActivity::finish() if (m_wallet_model) { // Check if wallet is HD-enabled (has mnemonic) and requires verification // Skip verification for blank wallets or wallets with disabled private keys - if (!m_wallet_model->wallet().hdEnabled() || + if (!m_wallet_model->wallet().hdEnabled() || m_wallet_model->wallet().privateKeysDisabled() || !m_wallet_model->wallet().canGetAddresses()) { // Not an HD wallet - skip verification @@ -291,7 +291,7 @@ void CreateWalletActivity::finish() Q_EMIT finished(); return; } - + // Unlock wallet if encrypted (needed to retrieve mnemonic) bool was_locked = false; bool was_unlocked_for_mixing = false; @@ -301,19 +301,19 @@ void CreateWalletActivity::finish() was_unlocked_for_mixing = (enc_status == WalletModel::UnlockedForMixingOnly); // Unlock fully (not just for mixing) to retrieve mnemonic if (!m_wallet_model->setWalletLocked(false, m_passphrase, false)) { - QMessageBox::warning(m_parent_widget, tr("Unlock failed"), + QMessageBox::warning(m_parent_widget, tr("Unlock failed"), tr("Failed to unlock wallet for mnemonic verification. Wallet creation completed but verification skipped.")); Q_EMIT created(m_wallet_model); Q_EMIT finished(); return; } } - + // Check if wallet has a mnemonic and requires verification SecureString mnemonic; SecureString mnemonic_passphrase; bool has_mnemonic = m_wallet_model->wallet().getMnemonic(mnemonic, mnemonic_passphrase); - + if (!has_mnemonic || mnemonic.empty()) { // No mnemonic found - log warning and skip verification restoreWalletLockState(was_locked, was_unlocked_for_mixing); @@ -326,18 +326,18 @@ void CreateWalletActivity::finish() Q_EMIT finished(); return; } - + // Wallet has mnemonic - show verification dialog MnemonicVerificationDialog verify_dialog(mnemonic, m_parent_widget); verify_dialog.setWindowModality(Qt::ApplicationModal); - + // Clear mnemonic from local variables after dialog has copied it // The dialog will manage its own copy securely const size_t mnemonic_size = mnemonic.size(); const size_t passphrase_size = mnemonic_passphrase.size(); mnemonic.assign(mnemonic_size, 0); mnemonic_passphrase.assign(passphrase_size, 0); - + if (verify_dialog.exec() == QDialog::Accepted) { // Verification successful restoreWalletLockState(was_locked, was_unlocked_for_mixing); @@ -362,11 +362,11 @@ void CreateWalletActivity::restoreWalletLockState(bool was_locked, bool was_unlo if (!was_locked && !was_unlocked_for_mixing) { return; // Wallet was not locked, nothing to restore } - + if (!m_wallet_model) { return; // Safety check: wallet model not available } - + if (was_unlocked_for_mixing) { // Restore previous mixing-only unlock state if (!m_wallet_model->setWalletLocked(true)) { diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index a64fc66dd3f8..2cd0177e0d88 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -528,14 +528,14 @@ class WalletImpl : public Wallet bool getMnemonic(SecureString& mnemonic_out, SecureString& mnemonic_passphrase_out) override { LOCK(m_wallet->cs_wallet); - + mnemonic_out.clear(); mnemonic_passphrase_out.clear(); - + if (m_wallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { return false; } - + if (m_wallet->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) { // Descriptor wallet for (auto spk_man : m_wallet->GetActiveScriptPubKeyMans()) { @@ -552,25 +552,25 @@ class WalletImpl : public Wallet if (!spk_man) { return false; } - + CHDChain hdChainCurrent; if (!spk_man->GetHDChain(hdChainCurrent)) { return false; } - + // Get decrypted HD chain if wallet is encrypted if (m_wallet->IsCrypted()) { if (!spk_man->GetDecryptedHDChain(hdChainCurrent)) { return false; } } - + SecureString ssMnemonic; SecureString ssMnemonicPassphrase; if (!hdChainCurrent.GetMnemonic(ssMnemonic, ssMnemonicPassphrase)) { return false; } - + mnemonic_out = ssMnemonic; mnemonic_passphrase_out = ssMnemonicPassphrase; return true; From fd3c1eb4b3d6fe06b2b453e10a64362cafec5330 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 6 Nov 2025 10:19:49 -0600 Subject: [PATCH 05/11] Simplify mnemonic retrieval --- src/wallet/interfaces.cpp | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index 2cd0177e0d88..a0f55c32fb2a 100644 --- a/src/wallet/interfaces.cpp +++ b/src/wallet/interfaces.cpp @@ -565,15 +565,7 @@ class WalletImpl : public Wallet } } - SecureString ssMnemonic; - SecureString ssMnemonicPassphrase; - if (!hdChainCurrent.GetMnemonic(ssMnemonic, ssMnemonicPassphrase)) { - return false; - } - - mnemonic_out = ssMnemonic; - mnemonic_passphrase_out = ssMnemonicPassphrase; - return true; + return hdChainCurrent.GetMnemonic(mnemonic_out, mnemonic_passphrase_out); } } std::unique_ptr handleUnload(UnloadFn fn) override From f79a750c52ae273587405ece7b594329c8ccc0e6 Mon Sep 17 00:00:00 2001 From: UdjinM6 Date: Thu, 6 Nov 2025 13:18:34 +0300 Subject: [PATCH 06/11] refactor: Move wallet creation dialog styles from C++ to CSS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move theme-independent styling from inline C++ code to general.css following Dash Core CSS architecture (general.css for layout, dark/light.css for colors). Changes: - Move font sizes, weights, and families to general.css - Move widget size constraints to general.css - Move label margins to general.css - Remove inline HTML style attributes from translated strings - Simplify C++ code to only handle dynamic theme colors via GUIUtil Benefits: - Better separation of concerns (styling vs logic) - Easier theme customization without recompiling - Consistent with Dash Core styling patterns - Cleaner, more maintainable C++ code 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/qt/mnemonicverificationdialog.cpp | 46 +++++++++--------------- src/qt/res/css/general.css | 51 +++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 30 deletions(-) diff --git a/src/qt/mnemonicverificationdialog.cpp b/src/qt/mnemonicverificationdialog.cpp index 418f72c3844f..30bc6df15c38 100644 --- a/src/qt/mnemonicverificationdialog.cpp +++ b/src/qt/mnemonicverificationdialog.cpp @@ -115,17 +115,14 @@ void MnemonicVerificationDialog::setupStep1() // Compact to content adjustSize(); - // Match visual hierarchy and tone of the improved mock - QString warningStyle = QString("font-size:17px; font-weight:700; ") + GUIUtil::getThemedStyleQString(GUIUtil::ThemedStyle::TS_ERROR); - QString instructionStyle = QString("font-size:14px; ") + GUIUtil::getThemedStyleQString(GUIUtil::ThemedStyle::TS_PRIMARY); + // Set warning and instruction text with themed colors + // Font sizes and weights are defined in general.css ui->warningLabel->setText( - tr("WARNING: If you lose your mnemonic seed phrase, you will lose access to your wallet forever. Write it down in a safe place and never share it with anyone.") - .arg(warningStyle) - ); + tr("WARNING: If you lose your mnemonic seed phrase, you will lose access to your wallet forever. Write it down in a safe place and never share it with anyone.")); + ui->warningLabel->setStyleSheet(GUIUtil::getThemedStyleQString(GUIUtil::ThemedStyle::TS_ERROR)); + ui->instructionLabel->setText( - tr("Please write down these words in order. You will need them to restore your wallet.") - .arg(instructionStyle) - ); + tr("Please write down these words in order. You will need them to restore your wallet.")); // Reduce extra padding to avoid an over-padded look if (auto outer = findChild("verticalLayout_step1")) { @@ -166,12 +163,7 @@ void MnemonicVerificationDialog::setupStep2() ui->word1Edit->clear(); ui->word2Edit->clear(); ui->word3Edit->clear(); - ui->word1Edit->setMaximumWidth(320); - ui->word2Edit->setMaximumWidth(320); - ui->word3Edit->setMaximumWidth(320); - ui->word1Status->setMinimumWidth(18); - ui->word2Status->setMinimumWidth(18); - ui->word3Status->setMinimumWidth(18); + // Widget sizes are defined in general.css ui->word1Label->setText(tr("Word #%1:").arg(m_selected_positions[0])); ui->word2Label->setText(tr("Word #%1:").arg(m_selected_positions[1])); @@ -191,10 +183,7 @@ void MnemonicVerificationDialog::setupStep2() if (QAbstractButton* cont = ui->buttonBox->button(QDialogButtonBox::Ok)) cont->setEnabled(false); if (ui->showMnemonicAgainButton) ui->showMnemonicAgainButton->hide(); - // Ensure verification label has minimal top spacing - if (ui->verificationLabel) { - ui->verificationLabel->setStyleSheet("QLabel { margin-top: 0px; margin-bottom: 4px; }"); - } + // Verification label styling is defined in general.css // Hide any existing title label if present if (auto titleLabel = findChild("verifyTitleLabel")) { @@ -303,16 +292,15 @@ void MnemonicVerificationDialog::updateWordValidation() const bool ok2 = !t2.isEmpty() && validateWord(t2, m_selected_positions[1]); const bool ok3 = !t3.isEmpty() && validateWord(t3, m_selected_positions[2]); + // Status labels show checkmarks/X marks with themed colors + // Font weight is defined in general.css auto setStatus = [](QLabel* lbl, bool filled, bool valid) { if (!lbl) return; if (!filled) { lbl->clear(); lbl->setStyleSheet(""); return; } - if (valid) { - lbl->setText("✓"); - lbl->setStyleSheet(QString("QLabel { %1 font-weight: 700; }").arg(GUIUtil::getThemedStyleQString(GUIUtil::ThemedStyle::TS_SUCCESS))); - } else { - lbl->setText("✗"); - lbl->setStyleSheet(QString("QLabel { %1 font-weight: 700; }").arg(GUIUtil::getThemedStyleQString(GUIUtil::ThemedStyle::TS_ERROR))); - } + lbl->setText(valid ? "✓" : "✗"); + lbl->setStyleSheet(valid ? + GUIUtil::getThemedStyleQString(GUIUtil::ThemedStyle::TS_SUCCESS) : + GUIUtil::getThemedStyleQString(GUIUtil::ThemedStyle::TS_ERROR)); }; setStatus(ui->word1Status, !t1.isEmpty(), ok1); setStatus(ui->word2Status, !t2.isEmpty(), ok2); @@ -405,7 +393,7 @@ void MnemonicVerificationDialog::buildMnemonicGrid(bool reveal) const int columns = (n >= 24) ? 4 : 3; const int rows = (n + columns - 1) / columns; - QFont mono; mono.setStyleHint(QFont::Monospace); mono.setFamily("Monospace"); mono.setPointSize(13); + // Font styling is defined in general.css m_gridLayout->setContentsMargins(6, 2, 6, 8); m_gridLayout->setHorizontalSpacing(32); m_gridLayout->setVerticalSpacing(7); @@ -415,7 +403,6 @@ void MnemonicVerificationDialog::buildMnemonicGrid(bool reveal) int idx = r * columns + c; if (idx >= n) break; const QString text = QString("%1. •••••••").arg(idx + 1, 2); QLabel* lbl = new QLabel(text); - lbl->setFont(mono); lbl->setTextInteractionFlags(Qt::TextSelectableByMouse); m_gridLayout->addWidget(lbl, r, c); } @@ -430,7 +417,7 @@ void MnemonicVerificationDialog::buildMnemonicGrid(bool reveal) const int columns = (n >= 24) ? 4 : 3; const int rows = (n + columns - 1) / columns; - QFont mono; mono.setStyleHint(QFont::Monospace); mono.setFamily("Monospace"); mono.setPointSize(13); + // Font styling is defined in general.css m_gridLayout->setContentsMargins(6, 2, 6, 8); m_gridLayout->setHorizontalSpacing(32); m_gridLayout->setVerticalSpacing(7); @@ -440,7 +427,6 @@ void MnemonicVerificationDialog::buildMnemonicGrid(bool reveal) int idx = r * columns + c; if (idx >= n) break; const QString text = QString("%1. %2").arg(idx + 1, 2).arg(words[idx]); QLabel* lbl = new QLabel(text); - lbl->setFont(mono); lbl->setTextInteractionFlags(Qt::TextSelectableByMouse); m_gridLayout->addWidget(lbl, r, c); } diff --git a/src/qt/res/css/general.css b/src/qt/res/css/general.css index c0444986d362..a04af4d4d805 100644 --- a/src/qt/res/css/general.css +++ b/src/qt/res/css/general.css @@ -1985,3 +1985,54 @@ QDialog#HelpMessageDialog QScrollBar:horizontal { } + +/** + * CreateWalletDialog (Layout) + */ + +QDialog#CreateWalletDialog QGroupBox#groupBox { + border-radius: 8px; + padding: 2px 12px 12px 12px; + margin-top: 0px; +} + +QDialog#CreateWalletDialog QGroupBox#groupBox::title { + padding: 0px; +} + +/** + * MnemonicVerificationDialog (Layout) + */ + +QDialog#MnemonicVerificationDialog QLabel#warningLabel { + font-size: 17px; + font-weight: 700; +} + +QDialog#MnemonicVerificationDialog QLabel#instructionLabel { + font-size: 14px; +} + +QDialog#MnemonicVerificationDialog QLabel#verificationLabel { + margin-top: 0px; + margin-bottom: 4px; +} + +QDialog#MnemonicVerificationDialog QLabel#word1Status, +QDialog#MnemonicVerificationDialog QLabel#word2Status, +QDialog#MnemonicVerificationDialog QLabel#word3Status { + font-weight: 700; + min-width: 18px; +} + +QDialog#MnemonicVerificationDialog QScrollArea#mnemonicScroll QWidget#mnemonicGridWidget QLabel { + font-family: monospace; + font-size: 13pt; +} + +QDialog#MnemonicVerificationDialog QLineEdit#word1Edit, +QDialog#MnemonicVerificationDialog QLineEdit#word2Edit, +QDialog#MnemonicVerificationDialog QLineEdit#word3Edit { + max-width: 320px; +} + From 3b16a88504f46523a2c3e011d70e947f7bb3e62f Mon Sep 17 00:00:00 2001 From: UdjinM6 Date: Thu, 6 Nov 2025 13:46:51 +0300 Subject: [PATCH 07/11] fix: Restore dialog size when going back from verification to word list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When clicking "Back" in the verification step (Step 2) to return to the word list display (Step 1), restore the original dialog size. Issue: Step 2 reduces minimum size to 460x280 for compact verification form, but setupStep1() didn't restore the original 550x360 size, causing the dialog to remain small when returning to the larger word list view. Fix: - Restore minimum size to 550x360 in setupStep1() - Resize to m_defaultSize when returning from Step 2 - Keep adjustSize() only for initial setup This ensures a consistent dialog size throughout the user flow. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/qt/mnemonicverificationdialog.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/qt/mnemonicverificationdialog.cpp b/src/qt/mnemonicverificationdialog.cpp index 30bc6df15c38..63cbf5b6374d 100644 --- a/src/qt/mnemonicverificationdialog.cpp +++ b/src/qt/mnemonicverificationdialog.cpp @@ -112,8 +112,16 @@ void MnemonicVerificationDialog::setupStep1() ui->writtenDownCheckbox->setChecked(false); m_mnemonic_revealed = false; ui->buttonBox->hide(); - // Compact to content - adjustSize(); + // Restore original minimum size (in case we came back from Step 2) + setMinimumSize(QSize(550, 360)); + + // Restore to default size if we have it (when coming back from Step 2) + if (m_defaultSize.isValid() && !m_defaultSize.isEmpty()) { + resize(m_defaultSize); + } else { + // Compact to content on first load + adjustSize(); + } // Set warning and instruction text with themed colors // Font sizes and weights are defined in general.css From 34914f10e4523cee8a15f4affb1c7bb88e534f63 Mon Sep 17 00:00:00 2001 From: UdjinM6 Date: Thu, 6 Nov 2025 13:33:30 +0300 Subject: [PATCH 08/11] feat: Add menu option to show recovery phrase for existing wallets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add "Show Recovery Phrase" menu item under Settings that allows users to view their mnemonic seed phrase for existing HD wallets with a streamlined view-only interface. Menu Integration: - Added Settings → Show Recovery Phrase menu option - Menu item positioned between "Change Passphrase" and "Unlock Wallet" - Enabled for HD wallets with private keys - Disabled for watch-only wallets (NoKeys status) View-Only Dialog Mode: - New optional viewOnly parameter in MnemonicVerificationDialog constructor - Simplified interface without verification step for viewing existing mnemonics - Window title: "Your Recovery Phrase" (vs "Save Your Mnemonic" for new wallets) - Button: Single "Close" button (Cancel button hidden) - Checkbox: "I have written down" checkbox hidden - Flow: Single-step view without word verification requirement - Adjusted warning text appropriate for viewing existing phrase Security Features: - Prompts for passphrase if wallet is encrypted (unlocks temporarily) - Validates wallet capabilities (HD enabled, has private keys) - Clears mnemonic from memory immediately after dialog creation - Properly restores wallet lock state after viewing: * Returns to locked state if was locked * Returns to unlocked-for-mixing state if was in that mode - Secure memory handling throughout with SecureString Error Handling: - Clear error messages for non-HD wallets - Clear error messages for watch-only wallets - Graceful handling of user cancellation during unlock - Proper error recovery with lock state restoration Backward Compatibility: - viewOnly parameter defaults to false - Original verification flow unchanged for new wallet creation - Existing MnemonicVerificationDialog behavior preserved This allows users to backup their recovery phrase after wallet creation without needing to recreate the wallet, with a cleaner UX that skips unnecessary verification steps when viewing an existing mnemonic. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/qt/bitcoingui.cpp | 6 +++ src/qt/bitcoingui.h | 1 + src/qt/mnemonicverificationdialog.cpp | 77 ++++++++++++++++++--------- src/qt/mnemonicverificationdialog.h | 3 +- src/qt/walletframe.cpp | 7 +++ src/qt/walletframe.h | 2 + src/qt/walletview.cpp | 50 +++++++++++++++++ src/qt/walletview.h | 2 + 8 files changed, 123 insertions(+), 25 deletions(-) diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index 18bf1be5b7e5..3921618469ea 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -394,6 +394,8 @@ void BitcoinGUI::createActions() backupWalletAction->setStatusTip(tr("Backup wallet to another location")); changePassphraseAction = new QAction(tr("&Change Passphrase…"), this); changePassphraseAction->setStatusTip(tr("Change the passphrase used for wallet encryption")); + showMnemonicAction = new QAction(tr("&Show Recovery Phrase…"), this); + showMnemonicAction->setStatusTip(tr("Show the recovery phrase (mnemonic seed) for this wallet")); unlockWalletAction = new QAction(tr("&Unlock Wallet…"), this); unlockWalletAction->setToolTip(tr("Unlock wallet")); lockWalletAction = new QAction(tr("&Lock Wallet"), this); @@ -502,6 +504,7 @@ void BitcoinGUI::createActions() connect(encryptWalletAction, &QAction::triggered, walletFrame, &WalletFrame::encryptWallet); connect(backupWalletAction, &QAction::triggered, walletFrame, &WalletFrame::backupWallet); connect(changePassphraseAction, &QAction::triggered, walletFrame, &WalletFrame::changePassphrase); + connect(showMnemonicAction, &QAction::triggered, walletFrame, &WalletFrame::showMnemonic); connect(unlockWalletAction, &QAction::triggered, walletFrame, &WalletFrame::unlockWallet); connect(lockWalletAction, &QAction::triggered, walletFrame, &WalletFrame::lockWallet); connect(signMessageAction, &QAction::triggered, [this]{ showNormalIfMinimized(); }); @@ -620,6 +623,7 @@ void BitcoinGUI::createMenuBar() { settings->addAction(encryptWalletAction); settings->addAction(changePassphraseAction); + settings->addAction(showMnemonicAction); settings->addAction(unlockWalletAction); settings->addAction(lockWalletAction); settings->addSeparator(); @@ -1040,6 +1044,7 @@ void BitcoinGUI::setWalletActionsEnabled(bool enabled) encryptWalletAction->setEnabled(enabled); backupWalletAction->setEnabled(enabled); changePassphraseAction->setEnabled(enabled); + showMnemonicAction->setEnabled(enabled); unlockWalletAction->setEnabled(enabled); lockWalletAction->setEnabled(enabled); signMessageAction->setEnabled(enabled); @@ -1936,6 +1941,7 @@ void BitcoinGUI::setEncryptionStatus(int status) encryptWalletAction->setChecked(false); changePassphraseAction->setEnabled(false); encryptWalletAction->setEnabled(false); + showMnemonicAction->setEnabled(false); break; case WalletModel::Unencrypted: labelWalletEncryptionIcon->show(); diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index e165738a8a32..08543872943e 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -157,6 +157,7 @@ class BitcoinGUI : public QMainWindow QAction* encryptWalletAction = nullptr; QAction* backupWalletAction = nullptr; QAction* changePassphraseAction = nullptr; + QAction* showMnemonicAction = nullptr; QAction* unlockWalletAction = nullptr; QAction* lockWalletAction = nullptr; QAction* aboutQtAction = nullptr; diff --git a/src/qt/mnemonicverificationdialog.cpp b/src/qt/mnemonicverificationdialog.cpp index 63cbf5b6374d..a3b7249c3e28 100644 --- a/src/qt/mnemonicverificationdialog.cpp +++ b/src/qt/mnemonicverificationdialog.cpp @@ -23,11 +23,12 @@ #include -MnemonicVerificationDialog::MnemonicVerificationDialog(const SecureString& mnemonic, QWidget *parent) : +MnemonicVerificationDialog::MnemonicVerificationDialog(const SecureString& mnemonic, QWidget *parent, bool viewOnly) : QDialog(parent, GUIUtil::dialog_flags), ui(new Ui::MnemonicVerificationDialog), m_mnemonic(mnemonic), - m_mnemonic_revealed(false) + m_mnemonic_revealed(false), + m_view_only(viewOnly) { ui->setupUi(this); @@ -39,7 +40,7 @@ MnemonicVerificationDialog::MnemonicVerificationDialog(const SecureString& mnemo setMinimumSize(QSize(550, 360)); resize(minimumSize()); - setWindowTitle(tr("Save Your Mnemonic")); + setWindowTitle(m_view_only ? tr("Your Recovery Phrase") : tr("Save Your Mnemonic")); // Words will be parsed on-demand to minimize exposure time in non-secure memory // m_words is intentionally left empty initially @@ -79,16 +80,19 @@ MnemonicVerificationDialog::MnemonicVerificationDialog(const SecureString& mnemo // Connections connect(ui->showMnemonicButton, &QPushButton::clicked, this, &MnemonicVerificationDialog::onShowMnemonicClicked); connect(ui->hideMnemonicButton, &QPushButton::clicked, this, &MnemonicVerificationDialog::onHideMnemonicClicked); - connect(ui->writtenDownCheckbox, &QCheckBox::toggled, this, [this](bool checked) { - if (checked && m_has_ever_revealed) setupStep2(); - }); - connect(ui->word1Edit, &QLineEdit::textChanged, this, &MnemonicVerificationDialog::onWord1Changed); - connect(ui->word2Edit, &QLineEdit::textChanged, this, &MnemonicVerificationDialog::onWord2Changed); - connect(ui->word3Edit, &QLineEdit::textChanged, this, &MnemonicVerificationDialog::onWord3Changed); - connect(ui->showMnemonicAgainButton, &QPushButton::clicked, this, &MnemonicVerificationDialog::onShowMnemonicAgainClicked); + + if (!m_view_only) { + connect(ui->writtenDownCheckbox, &QCheckBox::toggled, this, [this](bool checked) { + if (checked && m_has_ever_revealed) setupStep2(); + }); + connect(ui->word1Edit, &QLineEdit::textChanged, this, &MnemonicVerificationDialog::onWord1Changed); + connect(ui->word2Edit, &QLineEdit::textChanged, this, &MnemonicVerificationDialog::onWord2Changed); + connect(ui->word3Edit, &QLineEdit::textChanged, this, &MnemonicVerificationDialog::onWord3Changed); + connect(ui->showMnemonicAgainButton, &QPushButton::clicked, this, &MnemonicVerificationDialog::onShowMnemonicAgainClicked); + } // Button box - ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Continue")); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(m_view_only ? tr("Close") : tr("Continue")); connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &MnemonicVerificationDialog::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &MnemonicVerificationDialog::reject); @@ -111,7 +115,20 @@ void MnemonicVerificationDialog::setupStep1() ui->writtenDownCheckbox->setEnabled(false); ui->writtenDownCheckbox->setChecked(false); m_mnemonic_revealed = false; - ui->buttonBox->hide(); + + // In view-only mode, hide the checkbox and show buttonBox immediately + if (m_view_only) { + ui->writtenDownCheckbox->hide(); + ui->buttonBox->show(); + // Hide Cancel button in view-only mode - only Close button is needed + if (QAbstractButton* cancelBtn = ui->buttonBox->button(QDialogButtonBox::Cancel)) { + cancelBtn->hide(); + } + } else { + ui->writtenDownCheckbox->show(); + ui->buttonBox->hide(); + } + // Restore original minimum size (in case we came back from Step 2) setMinimumSize(QSize(550, 360)); @@ -125,12 +142,19 @@ void MnemonicVerificationDialog::setupStep1() // Set warning and instruction text with themed colors // Font sizes and weights are defined in general.css - ui->warningLabel->setText( - tr("WARNING: If you lose your mnemonic seed phrase, you will lose access to your wallet forever. Write it down in a safe place and never share it with anyone.")); - ui->warningLabel->setStyleSheet(GUIUtil::getThemedStyleQString(GUIUtil::ThemedStyle::TS_ERROR)); - - ui->instructionLabel->setText( - tr("Please write down these words in order. You will need them to restore your wallet.")); + if (m_view_only) { + ui->warningLabel->setText( + tr("WARNING: Never share your recovery phrase with anyone. Store it securely offline.")); + ui->warningLabel->setStyleSheet(GUIUtil::getThemedStyleQString(GUIUtil::ThemedStyle::TS_ERROR)); + ui->instructionLabel->setText( + tr("These words can restore your wallet. Keep them safe and private.")); + } else { + ui->warningLabel->setText( + tr("WARNING: If you lose your mnemonic seed phrase, you will lose access to your wallet forever. Write it down in a safe place and never share it with anyone.")); + ui->warningLabel->setStyleSheet(GUIUtil::getThemedStyleQString(GUIUtil::ThemedStyle::TS_ERROR)); + ui->instructionLabel->setText( + tr("Please write down these words in order. You will need them to restore your wallet.")); + } // Reduce extra padding to avoid an over-padded look if (auto outer = findChild("verticalLayout_step1")) { @@ -250,7 +274,9 @@ void MnemonicVerificationDialog::onShowMnemonicClicked() buildMnemonicGrid(true); ui->showMnemonicButton->hide(); ui->hideMnemonicButton->show(); - ui->writtenDownCheckbox->setEnabled(true); + if (!m_view_only) { + ui->writtenDownCheckbox->setEnabled(true); + } m_mnemonic_revealed = true; m_has_ever_revealed = true; } @@ -320,11 +346,14 @@ void MnemonicVerificationDialog::updateWordValidation() void MnemonicVerificationDialog::accept() { - if (!validateWord(ui->word1Edit->text().trimmed().toLower(), m_selected_positions[0]) || - !validateWord(ui->word2Edit->text().trimmed().toLower(), m_selected_positions[1]) || - !validateWord(ui->word3Edit->text().trimmed().toLower(), m_selected_positions[2])) { - QMessageBox::warning(this, tr("Verification Failed"), tr("One or more words are incorrect. Please try again.")); - return; + // In view-only mode, skip verification + if (!m_view_only) { + if (!validateWord(ui->word1Edit->text().trimmed().toLower(), m_selected_positions[0]) || + !validateWord(ui->word2Edit->text().trimmed().toLower(), m_selected_positions[1]) || + !validateWord(ui->word3Edit->text().trimmed().toLower(), m_selected_positions[2])) { + QMessageBox::warning(this, tr("Verification Failed"), tr("One or more words are incorrect. Please try again.")); + return; + } } QDialog::accept(); } diff --git a/src/qt/mnemonicverificationdialog.h b/src/qt/mnemonicverificationdialog.h index c58b7529a2de..efc326f9cafd 100644 --- a/src/qt/mnemonicverificationdialog.h +++ b/src/qt/mnemonicverificationdialog.h @@ -20,7 +20,7 @@ class MnemonicVerificationDialog : public QDialog Q_OBJECT public: - explicit MnemonicVerificationDialog(const SecureString& mnemonic, QWidget *parent = nullptr); + explicit MnemonicVerificationDialog(const SecureString& mnemonic, QWidget *parent = nullptr, bool viewOnly = false); ~MnemonicVerificationDialog(); void accept() override; @@ -51,6 +51,7 @@ private Q_SLOTS: QList m_selected_positions; bool m_mnemonic_revealed; bool m_has_ever_revealed{false}; + bool m_view_only{false}; class QGridLayout* m_gridLayout{nullptr}; QSize m_defaultSize; }; diff --git a/src/qt/walletframe.cpp b/src/qt/walletframe.cpp index 9373ab6119df..417847a6fe68 100644 --- a/src/qt/walletframe.cpp +++ b/src/qt/walletframe.cpp @@ -310,6 +310,13 @@ void WalletFrame::changePassphrase() walletView->changePassphrase(); } +void WalletFrame::showMnemonic() +{ + WalletView *walletView = currentWalletView(); + if (walletView) + walletView->showMnemonic(); +} + void WalletFrame::unlockWallet() { WalletView *walletView = currentWalletView(); diff --git a/src/qt/walletframe.h b/src/qt/walletframe.h index f95000d68d2d..f2820c7dd410 100644 --- a/src/qt/walletframe.h +++ b/src/qt/walletframe.h @@ -100,6 +100,8 @@ public Q_SLOTS: void backupWallet(); /** Change encrypted wallet passphrase */ void changePassphrase(); + /** Show wallet mnemonic/recovery phrase */ + void showMnemonic(); /** Ask for passphrase to unlock wallet temporarily */ void unlockWallet(); /** Lock wallet */ diff --git a/src/qt/walletview.cpp b/src/qt/walletview.cpp index 813a2e01c226..89433046bb54 100644 --- a/src/qt/walletview.cpp +++ b/src/qt/walletview.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,7 @@ #include #include +#include #include #include #include @@ -325,6 +327,54 @@ void WalletView::changePassphrase() GUIUtil::ShowModalDialogAsynchronously(dlg); } +void WalletView::showMnemonic() +{ + // Check if wallet supports mnemonic retrieval + if (walletModel->wallet().privateKeysDisabled()) { + QMessageBox::warning(this, tr("No Recovery Phrase"), + tr("This wallet does not have private keys and therefore has no recovery phrase.")); + return; + } + + if (!walletModel->wallet().hdEnabled()) { + QMessageBox::warning(this, tr("No Recovery Phrase"), + tr("This wallet was not created with HD (Hierarchical Deterministic) mode and does not have a recovery phrase.")); + return; + } + + // Request unlock if needed - UnlockContext will restore lock state on destruction + WalletModel::UnlockContext ctx(walletModel->requestUnlock()); + if (!ctx.isValid()) { + // User cancelled unlock + return; + } + + // Retrieve mnemonic + SecureString mnemonic; + SecureString mnemonic_passphrase; + bool has_mnemonic = walletModel->wallet().getMnemonic(mnemonic, mnemonic_passphrase); + + if (!has_mnemonic || mnemonic.empty()) { + QMessageBox::warning(this, tr("Mnemonic Retrieval Failed"), + tr("Could not retrieve the recovery phrase from this wallet.")); + return; + } + + // Show mnemonic verification dialog in view-only mode (no verification required) + MnemonicVerificationDialog verify_dialog(mnemonic, this, true); + verify_dialog.setWindowModality(Qt::ApplicationModal); + + // Clear mnemonic from local variables after dialog has copied it + const size_t mnemonic_size = mnemonic.size(); + const size_t passphrase_size = mnemonic_passphrase.size(); + mnemonic.assign(mnemonic_size, 0); + mnemonic_passphrase.assign(passphrase_size, 0); + + verify_dialog.exec(); + + // UnlockContext destructor will automatically restore the wallet lock state +} + void WalletView::unlockWallet(bool fForMixingOnly) { // Unlock wallet when requested by wallet model diff --git a/src/qt/walletview.h b/src/qt/walletview.h index 4ff82cbf4b8c..328b42821d06 100644 --- a/src/qt/walletview.h +++ b/src/qt/walletview.h @@ -107,6 +107,8 @@ public Q_SLOTS: void backupWallet(); /** Change encrypted wallet passphrase */ void changePassphrase(); + /** Show wallet mnemonic/recovery phrase */ + void showMnemonic(); /** Ask for passphrase to unlock wallet temporarily */ void unlockWallet(bool fAnonymizeOnly=false); /** Lock wallet */ From a0b15cfaed5515aafe242f410a64374cc98582e4 Mon Sep 17 00:00:00 2001 From: UdjinM6 Date: Thu, 6 Nov 2025 17:23:48 +0300 Subject: [PATCH 09/11] refactor: Simplify wallet lock state handling in CreateWalletActivity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplify the lock state tracking logic in CreateWalletActivity::finish() since mixing-only unlock state is impossible for newly created wallets. Changes: - Use getEncryptionStatus() instead of tracking multiple state variables - Replace was_locked and was_unlocked_for_mixing with single was_locked check - Remove restoreWalletLockState() helper method - inline simple lock call - Add comment explaining why mixing state is impossible at creation time Before: Tracked both was_locked and was_unlocked_for_mixing states After: Only track was_locked since new wallets can only be: * Unencrypted (unlocked) * Encrypted and locked (need to unlock to show mnemonic) This is clearer and removes unnecessary complexity for an impossible state. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/qt/walletcontroller.cpp | 54 ++++++++++--------------------------- src/qt/walletcontroller.h | 1 - 2 files changed, 14 insertions(+), 41 deletions(-) diff --git a/src/qt/walletcontroller.cpp b/src/qt/walletcontroller.cpp index 0a1e194d13a9..79df868be617 100644 --- a/src/qt/walletcontroller.cpp +++ b/src/qt/walletcontroller.cpp @@ -293,13 +293,11 @@ void CreateWalletActivity::finish() } // Unlock wallet if encrypted (needed to retrieve mnemonic) - bool was_locked = false; - bool was_unlocked_for_mixing = false; - WalletModel::EncryptionStatus enc_status = m_wallet_model->getEncryptionStatus(); - if (enc_status == WalletModel::Locked || enc_status == WalletModel::UnlockedForMixingOnly) { - was_locked = (enc_status == WalletModel::Locked); - was_unlocked_for_mixing = (enc_status == WalletModel::UnlockedForMixingOnly); - // Unlock fully (not just for mixing) to retrieve mnemonic + // Note: Newly created wallet can only be locked (if encrypted) or unencrypted. + // Mixing-only unlock state is not possible at wallet creation time. + const bool was_locked = (m_wallet_model->getEncryptionStatus() == WalletModel::Locked); + if (was_locked) { + // Unlock to retrieve mnemonic using passphrase from wallet creation if (!m_wallet_model->setWalletLocked(false, m_passphrase, false)) { QMessageBox::warning(m_parent_widget, tr("Unlock failed"), tr("Failed to unlock wallet for mnemonic verification. Wallet creation completed but verification skipped.")); @@ -316,7 +314,9 @@ void CreateWalletActivity::finish() if (!has_mnemonic || mnemonic.empty()) { // No mnemonic found - log warning and skip verification - restoreWalletLockState(was_locked, was_unlocked_for_mixing); + if (was_locked) { + m_wallet_model->setWalletLocked(true); + } // Clear sensitive data before showing message mnemonic.assign(mnemonic.size(), 0); mnemonic_passphrase.assign(mnemonic_passphrase.size(), 0); @@ -340,11 +340,15 @@ void CreateWalletActivity::finish() if (verify_dialog.exec() == QDialog::Accepted) { // Verification successful - restoreWalletLockState(was_locked, was_unlocked_for_mixing); + if (was_locked) { + m_wallet_model->setWalletLocked(true); + } Q_EMIT created(m_wallet_model); } else { // User cancelled verification - restoreWalletLockState(was_locked, was_unlocked_for_mixing); + if (was_locked) { + m_wallet_model->setWalletLocked(true); + } QMessageBox::warning(m_parent_widget, tr("Verification cancelled"), tr("You cancelled mnemonic verification. Please make sure you have saved your mnemonic phrase safely.")); Q_EMIT created(m_wallet_model); @@ -357,36 +361,6 @@ void CreateWalletActivity::finish() Q_EMIT finished(); } -void CreateWalletActivity::restoreWalletLockState(bool was_locked, bool was_unlocked_for_mixing) -{ - if (!was_locked && !was_unlocked_for_mixing) { - return; // Wallet was not locked, nothing to restore - } - - if (!m_wallet_model) { - return; // Safety check: wallet model not available - } - - if (was_unlocked_for_mixing) { - // Restore previous mixing-only unlock state - if (!m_wallet_model->setWalletLocked(true)) { - QMessageBox::warning(m_parent_widget, tr("Warning"), - tr("Failed to lock wallet. Please lock it manually.")); - return; - } - if (!m_wallet_model->setWalletLocked(false, m_passphrase, true)) { - QMessageBox::warning(m_parent_widget, tr("Warning"), - tr("Failed to restore mixing unlock state.")); - } - } else { - // Restore fully locked state - if (!m_wallet_model->setWalletLocked(true)) { - QMessageBox::warning(m_parent_widget, tr("Warning"), - tr("Failed to lock wallet. Please lock it manually.")); - } - } -} - void CreateWalletActivity::create() { m_create_wallet_dialog = new CreateWalletDialog(m_parent_widget); diff --git a/src/qt/walletcontroller.h b/src/qt/walletcontroller.h index 6d269eda0afb..ccb7dbc62d51 100644 --- a/src/qt/walletcontroller.h +++ b/src/qt/walletcontroller.h @@ -123,7 +123,6 @@ class CreateWalletActivity : public WalletControllerActivity void askPassphrase(); void createWallet(); void finish(); - void restoreWalletLockState(bool was_locked, bool was_unlocked_for_mixing); SecureString m_passphrase; CreateWalletDialog* m_create_wallet_dialog{nullptr}; From 5024589ccc8b2e2b8effa0d7fca4edfc8bdbc977 Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 6 Nov 2025 22:21:33 -0600 Subject: [PATCH 10/11] doc: Add release notes for PR #6946 --- doc/release-notes-6946.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 doc/release-notes-6946.md diff --git a/doc/release-notes-6946.md b/doc/release-notes-6946.md new file mode 100644 index 000000000000..f522978dba54 --- /dev/null +++ b/doc/release-notes-6946.md @@ -0,0 +1,6 @@ +GUI changes +-------- + +- A mnemonic verification dialog is now shown after creating a new HD wallet, requiring users to verify they have written down their recovery phrase (#6946). +- A new menu item "Show Recovery Phrase…" has been added to the Settings menu to view the recovery phrase for existing HD wallets (#6946). + From 9531028434fe729488610302aac46df1905f045a Mon Sep 17 00:00:00 2001 From: pasta Date: Thu, 6 Nov 2025 22:25:26 -0600 Subject: [PATCH 11/11] security: Store mnemonic words in secure memory Replace QStringList with std::vector for m_words member to ensure mnemonic seed words are stored in secure memory that: - Locks memory to prevent paging to disk - Automatically zeros memory on deallocation - Uses secure allocators throughout the lifetime of sensitive data This addresses a security concern where mnemonic words were stored in non-secure memory (QStringList) which does not guarantee secure memory allocation or zeroing on deallocation. --- src/qt/mnemonicverificationdialog.cpp | 53 ++++++++++++++++++++------- src/qt/mnemonicverificationdialog.h | 5 ++- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/src/qt/mnemonicverificationdialog.cpp b/src/qt/mnemonicverificationdialog.cpp index a3b7249c3e28..365b8e6e5dc1 100644 --- a/src/qt/mnemonicverificationdialog.cpp +++ b/src/qt/mnemonicverificationdialog.cpp @@ -309,11 +309,18 @@ bool MnemonicVerificationDialog::validateWord(const QString& word, int position) // Parse words on-demand for validation (minimizes exposure time) // Words are kept in memory during step 2 (verification) and step 1 (when revealed) // They are only cleared when explicitly hiding in step 1 or on dialog destruction - QStringList words = parseWords(); - if (position < 1 || position > words.size()) { + std::vector words = parseWords(); + if (position < 1 || position > static_cast(words.size())) { return false; } - return word == words[position - 1].toLower(); + // Convert SecureString to QString temporarily for comparison + QString secureWord = QString::fromStdString(std::string(words[position - 1].begin(), words[position - 1].end())); + bool result = word == secureWord.toLower(); + // Clear temporary QString immediately + secureWord.fill(QChar(0)); + secureWord.clear(); + secureWord.squeeze(); + return result; } void MnemonicVerificationDialog::updateWordValidation() @@ -364,20 +371,34 @@ void MnemonicVerificationDialog::clearMnemonic() m_mnemonic.assign(m_mnemonic.size(), 0); } -QStringList MnemonicVerificationDialog::parseWords() +std::vector MnemonicVerificationDialog::parseWords() { // If words are already parsed, reuse them (for step 2 validation or step 1 display) - if (!m_words.isEmpty()) { + if (!m_words.empty()) { return m_words; } // Parse words from secure mnemonic string QString mnemonicStr = QString::fromStdString(std::string(m_mnemonic.begin(), m_mnemonic.end())); - m_words = mnemonicStr.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); + QStringList wordList = mnemonicStr.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); + + // Convert to SecureString vector for secure storage + m_words.clear(); + m_words.reserve(wordList.size()); + for (const QString& word : wordList) { + std::string wordStd = word.toStdString(); + SecureString secureWord; + secureWord.assign(std::string_view{wordStd}); + m_words.push_back(secureWord); + // Clear temporary std::string + wordStd.assign(wordStd.size(), 0); + } // Clear the temporary QString immediately after parsing + mnemonicStr.fill(QChar(0)); mnemonicStr.clear(); mnemonicStr.squeeze(); // Release memory + wordList.clear(); return m_words; } @@ -385,26 +406,26 @@ QStringList MnemonicVerificationDialog::parseWords() void MnemonicVerificationDialog::clearWordsSecurely() { // Securely clear each word string by overwriting before clearing - for (QString& word : m_words) { + for (SecureString& word : m_words) { // Overwrite with zeros before clearing - word.fill(QChar(0)); + word.assign(word.size(), 0); word.clear(); - word.squeeze(); // Release memory } m_words.clear(); } int MnemonicVerificationDialog::getWordCount() const { - // Count words without parsing them into QStringList + // Count words without parsing them into vector // This avoids storing words in non-secure memory unnecessarily - if (m_words.isEmpty()) { + if (m_words.empty()) { QString mnemonicStr = QString::fromStdString(std::string(m_mnemonic.begin(), m_mnemonic.end())); QStringList words = mnemonicStr.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); int count = words.size(); // Clear immediately mnemonicStr.clear(); mnemonicStr.squeeze(); + words.clear(); return count; } return m_words.size(); @@ -421,7 +442,7 @@ void MnemonicVerificationDialog::buildMnemonicGrid(bool reveal) } // Parse words only when revealing (when needed for display) - QStringList words; + std::vector words; if (reveal) { words = parseWords(); } else { @@ -462,10 +483,16 @@ void MnemonicVerificationDialog::buildMnemonicGrid(bool reveal) for (int r = 0; r < rows; ++r) { for (int c = 0; c < columns; ++c) { int idx = r * columns + c; if (idx >= n) break; - const QString text = QString("%1. %2").arg(idx + 1, 2).arg(words[idx]); + // Convert SecureString to QString temporarily for display + QString wordStr = QString::fromStdString(std::string(words[idx].begin(), words[idx].end())); + const QString text = QString("%1. %2").arg(idx + 1, 2).arg(wordStr); QLabel* lbl = new QLabel(text); lbl->setTextInteractionFlags(Qt::TextSelectableByMouse); m_gridLayout->addWidget(lbl, r, c); + // Clear temporary QString immediately after use + wordStr.fill(QChar(0)); + wordStr.clear(); + wordStr.squeeze(); } } diff --git a/src/qt/mnemonicverificationdialog.h b/src/qt/mnemonicverificationdialog.h index efc326f9cafd..309912201c15 100644 --- a/src/qt/mnemonicverificationdialog.h +++ b/src/qt/mnemonicverificationdialog.h @@ -9,6 +9,7 @@ #include #include +#include namespace Ui { class MnemonicVerificationDialog; @@ -41,13 +42,13 @@ private Q_SLOTS: bool validateWord(const QString& word, int position); void clearMnemonic(); void buildMnemonicGrid(bool reveal); - QStringList parseWords(); + std::vector parseWords(); void clearWordsSecurely(); int getWordCount() const; Ui::MnemonicVerificationDialog *ui; SecureString m_mnemonic; - QStringList m_words; + std::vector m_words; QList m_selected_positions; bool m_mnemonic_revealed; bool m_has_ever_revealed{false};