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). + 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/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/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/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; diff --git a/src/qt/mnemonicverificationdialog.cpp b/src/qt/mnemonicverificationdialog.cpp new file mode 100644 index 000000000000..365b8e6e5dc1 --- /dev/null +++ b/src/qt/mnemonicverificationdialog.cpp @@ -0,0 +1,502 @@ +// 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 + +#include + +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_view_only(viewOnly) +{ + 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(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 + + // 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); + + 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(m_view_only ? tr("Close") : 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; + + // 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)); + + // 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 + 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")) { + 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(); + + // 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(); + ui->word3Edit->clear(); + // 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])); + 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(); + + // Verification label styling is defined in general.css + + // 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 = 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) { + 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(); + if (!m_view_only) { + 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 + std::vector words = parseWords(); + if (position < 1 || position > static_cast(words.size())) { + return false; + } + // 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() +{ + 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]); + + // 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; } + 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); + 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() +{ + // 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(); +} + +void MnemonicVerificationDialog::clearMnemonic() +{ + clearWordsSecurely(); + m_mnemonic.assign(m_mnemonic.size(), 0); +} + +std::vector MnemonicVerificationDialog::parseWords() +{ + // If words are already parsed, reuse them (for step 2 validation or step 1 display) + 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())); + 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; +} + +void MnemonicVerificationDialog::clearWordsSecurely() +{ + // Securely clear each word string by overwriting before clearing + for (SecureString& word : m_words) { + // Overwrite with zeros before clearing + word.assign(word.size(), 0); + word.clear(); + } + m_words.clear(); +} + +int MnemonicVerificationDialog::getWordCount() const +{ + // Count words without parsing them into vector + // This avoids storing words in non-secure memory unnecessarily + 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(); +} + +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) + std::vector 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; + + // Font styling is defined in general.css + 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->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; + + // Font styling is defined in general.css + 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; + // 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(); + } + } + + m_gridLayout->setRowMinimumHeight(rows, 12); +} + + diff --git a/src/qt/mnemonicverificationdialog.h b/src/qt/mnemonicverificationdialog.h new file mode 100644 index 000000000000..309912201c15 --- /dev/null +++ b/src/qt/mnemonicverificationdialog.h @@ -0,0 +1,61 @@ +// 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 +#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, bool viewOnly = false); + ~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); + std::vector parseWords(); + void clearWordsSecurely(); + int getWordCount() const; + + Ui::MnemonicVerificationDialog *ui; + SecureString m_mnemonic; + std::vector m_words; + 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; +}; + +#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/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; +} + 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..79df868be617 100644 --- a/src/qt/walletcontroller.cpp +++ b/src/qt/walletcontroller.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -279,7 +280,83 @@ 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) + // 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.")); + 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 + 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); + 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 + if (was_locked) { + m_wallet_model->setWalletLocked(true); + } + Q_EMIT created(m_wallet_model); + } else { + // User cancelled verification + 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); + } + } else { + // Wallet creation failed - no wallet model + // Already showed error message above + } Q_EMIT finished(); } 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 */ diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp index 4c134c76220e..a0f55c32fb2a 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,49 @@ 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; + } + } + + return hdChainCurrent.GetMnemonic(mnemonic_out, mnemonic_passphrase_out); + } + } std::unique_ptr handleUnload(UnloadFn fn) override { return MakeHandler(m_wallet->NotifyUnload.connect(fn));