Skip to content

Commit 34914f1

Browse files
UdjinM6claude
authored andcommitted
feat: Add menu option to show recovery phrase for existing wallets
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 <noreply@anthropic.com>
1 parent 3b16a88 commit 34914f1

File tree

8 files changed

+123
-25
lines changed

8 files changed

+123
-25
lines changed

src/qt/bitcoingui.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,8 @@ void BitcoinGUI::createActions()
394394
backupWalletAction->setStatusTip(tr("Backup wallet to another location"));
395395
changePassphraseAction = new QAction(tr("&Change Passphrase…"), this);
396396
changePassphraseAction->setStatusTip(tr("Change the passphrase used for wallet encryption"));
397+
showMnemonicAction = new QAction(tr("&Show Recovery Phrase…"), this);
398+
showMnemonicAction->setStatusTip(tr("Show the recovery phrase (mnemonic seed) for this wallet"));
397399
unlockWalletAction = new QAction(tr("&Unlock Wallet…"), this);
398400
unlockWalletAction->setToolTip(tr("Unlock wallet"));
399401
lockWalletAction = new QAction(tr("&Lock Wallet"), this);
@@ -502,6 +504,7 @@ void BitcoinGUI::createActions()
502504
connect(encryptWalletAction, &QAction::triggered, walletFrame, &WalletFrame::encryptWallet);
503505
connect(backupWalletAction, &QAction::triggered, walletFrame, &WalletFrame::backupWallet);
504506
connect(changePassphraseAction, &QAction::triggered, walletFrame, &WalletFrame::changePassphrase);
507+
connect(showMnemonicAction, &QAction::triggered, walletFrame, &WalletFrame::showMnemonic);
505508
connect(unlockWalletAction, &QAction::triggered, walletFrame, &WalletFrame::unlockWallet);
506509
connect(lockWalletAction, &QAction::triggered, walletFrame, &WalletFrame::lockWallet);
507510
connect(signMessageAction, &QAction::triggered, [this]{ showNormalIfMinimized(); });
@@ -620,6 +623,7 @@ void BitcoinGUI::createMenuBar()
620623
{
621624
settings->addAction(encryptWalletAction);
622625
settings->addAction(changePassphraseAction);
626+
settings->addAction(showMnemonicAction);
623627
settings->addAction(unlockWalletAction);
624628
settings->addAction(lockWalletAction);
625629
settings->addSeparator();
@@ -1040,6 +1044,7 @@ void BitcoinGUI::setWalletActionsEnabled(bool enabled)
10401044
encryptWalletAction->setEnabled(enabled);
10411045
backupWalletAction->setEnabled(enabled);
10421046
changePassphraseAction->setEnabled(enabled);
1047+
showMnemonicAction->setEnabled(enabled);
10431048
unlockWalletAction->setEnabled(enabled);
10441049
lockWalletAction->setEnabled(enabled);
10451050
signMessageAction->setEnabled(enabled);
@@ -1936,6 +1941,7 @@ void BitcoinGUI::setEncryptionStatus(int status)
19361941
encryptWalletAction->setChecked(false);
19371942
changePassphraseAction->setEnabled(false);
19381943
encryptWalletAction->setEnabled(false);
1944+
showMnemonicAction->setEnabled(false);
19391945
break;
19401946
case WalletModel::Unencrypted:
19411947
labelWalletEncryptionIcon->show();

src/qt/bitcoingui.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ class BitcoinGUI : public QMainWindow
157157
QAction* encryptWalletAction = nullptr;
158158
QAction* backupWalletAction = nullptr;
159159
QAction* changePassphraseAction = nullptr;
160+
QAction* showMnemonicAction = nullptr;
160161
QAction* unlockWalletAction = nullptr;
161162
QAction* lockWalletAction = nullptr;
162163
QAction* aboutQtAction = nullptr;

src/qt/mnemonicverificationdialog.cpp

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@
2323

2424
#include <algorithm>
2525

26-
MnemonicVerificationDialog::MnemonicVerificationDialog(const SecureString& mnemonic, QWidget *parent) :
26+
MnemonicVerificationDialog::MnemonicVerificationDialog(const SecureString& mnemonic, QWidget *parent, bool viewOnly) :
2727
QDialog(parent, GUIUtil::dialog_flags),
2828
ui(new Ui::MnemonicVerificationDialog),
2929
m_mnemonic(mnemonic),
30-
m_mnemonic_revealed(false)
30+
m_mnemonic_revealed(false),
31+
m_view_only(viewOnly)
3132
{
3233
ui->setupUi(this);
3334

@@ -39,7 +40,7 @@ MnemonicVerificationDialog::MnemonicVerificationDialog(const SecureString& mnemo
3940
setMinimumSize(QSize(550, 360));
4041
resize(minimumSize());
4142

42-
setWindowTitle(tr("Save Your Mnemonic"));
43+
setWindowTitle(m_view_only ? tr("Your Recovery Phrase") : tr("Save Your Mnemonic"));
4344

4445
// Words will be parsed on-demand to minimize exposure time in non-secure memory
4546
// m_words is intentionally left empty initially
@@ -79,16 +80,19 @@ MnemonicVerificationDialog::MnemonicVerificationDialog(const SecureString& mnemo
7980
// Connections
8081
connect(ui->showMnemonicButton, &QPushButton::clicked, this, &MnemonicVerificationDialog::onShowMnemonicClicked);
8182
connect(ui->hideMnemonicButton, &QPushButton::clicked, this, &MnemonicVerificationDialog::onHideMnemonicClicked);
82-
connect(ui->writtenDownCheckbox, &QCheckBox::toggled, this, [this](bool checked) {
83-
if (checked && m_has_ever_revealed) setupStep2();
84-
});
85-
connect(ui->word1Edit, &QLineEdit::textChanged, this, &MnemonicVerificationDialog::onWord1Changed);
86-
connect(ui->word2Edit, &QLineEdit::textChanged, this, &MnemonicVerificationDialog::onWord2Changed);
87-
connect(ui->word3Edit, &QLineEdit::textChanged, this, &MnemonicVerificationDialog::onWord3Changed);
88-
connect(ui->showMnemonicAgainButton, &QPushButton::clicked, this, &MnemonicVerificationDialog::onShowMnemonicAgainClicked);
83+
84+
if (!m_view_only) {
85+
connect(ui->writtenDownCheckbox, &QCheckBox::toggled, this, [this](bool checked) {
86+
if (checked && m_has_ever_revealed) setupStep2();
87+
});
88+
connect(ui->word1Edit, &QLineEdit::textChanged, this, &MnemonicVerificationDialog::onWord1Changed);
89+
connect(ui->word2Edit, &QLineEdit::textChanged, this, &MnemonicVerificationDialog::onWord2Changed);
90+
connect(ui->word3Edit, &QLineEdit::textChanged, this, &MnemonicVerificationDialog::onWord3Changed);
91+
connect(ui->showMnemonicAgainButton, &QPushButton::clicked, this, &MnemonicVerificationDialog::onShowMnemonicAgainClicked);
92+
}
8993

9094
// Button box
91-
ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Continue"));
95+
ui->buttonBox->button(QDialogButtonBox::Ok)->setText(m_view_only ? tr("Close") : tr("Continue"));
9296
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &MnemonicVerificationDialog::accept);
9397
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &MnemonicVerificationDialog::reject);
9498

@@ -111,7 +115,20 @@ void MnemonicVerificationDialog::setupStep1()
111115
ui->writtenDownCheckbox->setEnabled(false);
112116
ui->writtenDownCheckbox->setChecked(false);
113117
m_mnemonic_revealed = false;
114-
ui->buttonBox->hide();
118+
119+
// In view-only mode, hide the checkbox and show buttonBox immediately
120+
if (m_view_only) {
121+
ui->writtenDownCheckbox->hide();
122+
ui->buttonBox->show();
123+
// Hide Cancel button in view-only mode - only Close button is needed
124+
if (QAbstractButton* cancelBtn = ui->buttonBox->button(QDialogButtonBox::Cancel)) {
125+
cancelBtn->hide();
126+
}
127+
} else {
128+
ui->writtenDownCheckbox->show();
129+
ui->buttonBox->hide();
130+
}
131+
115132
// Restore original minimum size (in case we came back from Step 2)
116133
setMinimumSize(QSize(550, 360));
117134

@@ -125,12 +142,19 @@ void MnemonicVerificationDialog::setupStep1()
125142

126143
// Set warning and instruction text with themed colors
127144
// Font sizes and weights are defined in general.css
128-
ui->warningLabel->setText(
129-
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."));
130-
ui->warningLabel->setStyleSheet(GUIUtil::getThemedStyleQString(GUIUtil::ThemedStyle::TS_ERROR));
131-
132-
ui->instructionLabel->setText(
133-
tr("Please write down these words in order. You will need them to restore your wallet."));
145+
if (m_view_only) {
146+
ui->warningLabel->setText(
147+
tr("WARNING: Never share your recovery phrase with anyone. Store it securely offline."));
148+
ui->warningLabel->setStyleSheet(GUIUtil::getThemedStyleQString(GUIUtil::ThemedStyle::TS_ERROR));
149+
ui->instructionLabel->setText(
150+
tr("These words can restore your wallet. Keep them safe and private."));
151+
} else {
152+
ui->warningLabel->setText(
153+
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."));
154+
ui->warningLabel->setStyleSheet(GUIUtil::getThemedStyleQString(GUIUtil::ThemedStyle::TS_ERROR));
155+
ui->instructionLabel->setText(
156+
tr("Please write down these words in order. You will need them to restore your wallet."));
157+
}
134158

135159
// Reduce extra padding to avoid an over-padded look
136160
if (auto outer = findChild<QVBoxLayout*>("verticalLayout_step1")) {
@@ -250,7 +274,9 @@ void MnemonicVerificationDialog::onShowMnemonicClicked()
250274
buildMnemonicGrid(true);
251275
ui->showMnemonicButton->hide();
252276
ui->hideMnemonicButton->show();
253-
ui->writtenDownCheckbox->setEnabled(true);
277+
if (!m_view_only) {
278+
ui->writtenDownCheckbox->setEnabled(true);
279+
}
254280
m_mnemonic_revealed = true;
255281
m_has_ever_revealed = true;
256282
}
@@ -320,11 +346,14 @@ void MnemonicVerificationDialog::updateWordValidation()
320346

321347
void MnemonicVerificationDialog::accept()
322348
{
323-
if (!validateWord(ui->word1Edit->text().trimmed().toLower(), m_selected_positions[0]) ||
324-
!validateWord(ui->word2Edit->text().trimmed().toLower(), m_selected_positions[1]) ||
325-
!validateWord(ui->word3Edit->text().trimmed().toLower(), m_selected_positions[2])) {
326-
QMessageBox::warning(this, tr("Verification Failed"), tr("One or more words are incorrect. Please try again."));
327-
return;
349+
// In view-only mode, skip verification
350+
if (!m_view_only) {
351+
if (!validateWord(ui->word1Edit->text().trimmed().toLower(), m_selected_positions[0]) ||
352+
!validateWord(ui->word2Edit->text().trimmed().toLower(), m_selected_positions[1]) ||
353+
!validateWord(ui->word3Edit->text().trimmed().toLower(), m_selected_positions[2])) {
354+
QMessageBox::warning(this, tr("Verification Failed"), tr("One or more words are incorrect. Please try again."));
355+
return;
356+
}
328357
}
329358
QDialog::accept();
330359
}

src/qt/mnemonicverificationdialog.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class MnemonicVerificationDialog : public QDialog
2020
Q_OBJECT
2121

2222
public:
23-
explicit MnemonicVerificationDialog(const SecureString& mnemonic, QWidget *parent = nullptr);
23+
explicit MnemonicVerificationDialog(const SecureString& mnemonic, QWidget *parent = nullptr, bool viewOnly = false);
2424
~MnemonicVerificationDialog();
2525

2626
void accept() override;
@@ -51,6 +51,7 @@ private Q_SLOTS:
5151
QList<int> m_selected_positions;
5252
bool m_mnemonic_revealed;
5353
bool m_has_ever_revealed{false};
54+
bool m_view_only{false};
5455
class QGridLayout* m_gridLayout{nullptr};
5556
QSize m_defaultSize;
5657
};

src/qt/walletframe.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,13 @@ void WalletFrame::changePassphrase()
310310
walletView->changePassphrase();
311311
}
312312

313+
void WalletFrame::showMnemonic()
314+
{
315+
WalletView *walletView = currentWalletView();
316+
if (walletView)
317+
walletView->showMnemonic();
318+
}
319+
313320
void WalletFrame::unlockWallet()
314321
{
315322
WalletView *walletView = currentWalletView();

src/qt/walletframe.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ public Q_SLOTS:
100100
void backupWallet();
101101
/** Change encrypted wallet passphrase */
102102
void changePassphrase();
103+
/** Show wallet mnemonic/recovery phrase */
104+
void showMnemonic();
103105
/** Ask for passphrase to unlock wallet temporarily */
104106
void unlockWallet();
105107
/** Lock wallet */

src/qt/walletview.cpp

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <qt/askpassphrasedialog.h>
1212
#include <qt/clientmodel.h>
1313
#include <qt/guiutil.h>
14+
#include <qt/mnemonicverificationdialog.h>
1415
#include <qt/optionsmodel.h>
1516
#include <qt/overviewpage.h>
1617
#include <qt/receivecoinsdialog.h>
@@ -27,6 +28,7 @@
2728

2829
#include <QHBoxLayout>
2930
#include <QLabel>
31+
#include <QMessageBox>
3032
#include <QProgressDialog>
3133
#include <QPushButton>
3234
#include <QSettings>
@@ -325,6 +327,54 @@ void WalletView::changePassphrase()
325327
GUIUtil::ShowModalDialogAsynchronously(dlg);
326328
}
327329

330+
void WalletView::showMnemonic()
331+
{
332+
// Check if wallet supports mnemonic retrieval
333+
if (walletModel->wallet().privateKeysDisabled()) {
334+
QMessageBox::warning(this, tr("No Recovery Phrase"),
335+
tr("This wallet does not have private keys and therefore has no recovery phrase."));
336+
return;
337+
}
338+
339+
if (!walletModel->wallet().hdEnabled()) {
340+
QMessageBox::warning(this, tr("No Recovery Phrase"),
341+
tr("This wallet was not created with HD (Hierarchical Deterministic) mode and does not have a recovery phrase."));
342+
return;
343+
}
344+
345+
// Request unlock if needed - UnlockContext will restore lock state on destruction
346+
WalletModel::UnlockContext ctx(walletModel->requestUnlock());
347+
if (!ctx.isValid()) {
348+
// User cancelled unlock
349+
return;
350+
}
351+
352+
// Retrieve mnemonic
353+
SecureString mnemonic;
354+
SecureString mnemonic_passphrase;
355+
bool has_mnemonic = walletModel->wallet().getMnemonic(mnemonic, mnemonic_passphrase);
356+
357+
if (!has_mnemonic || mnemonic.empty()) {
358+
QMessageBox::warning(this, tr("Mnemonic Retrieval Failed"),
359+
tr("Could not retrieve the recovery phrase from this wallet."));
360+
return;
361+
}
362+
363+
// Show mnemonic verification dialog in view-only mode (no verification required)
364+
MnemonicVerificationDialog verify_dialog(mnemonic, this, true);
365+
verify_dialog.setWindowModality(Qt::ApplicationModal);
366+
367+
// Clear mnemonic from local variables after dialog has copied it
368+
const size_t mnemonic_size = mnemonic.size();
369+
const size_t passphrase_size = mnemonic_passphrase.size();
370+
mnemonic.assign(mnemonic_size, 0);
371+
mnemonic_passphrase.assign(passphrase_size, 0);
372+
373+
verify_dialog.exec();
374+
375+
// UnlockContext destructor will automatically restore the wallet lock state
376+
}
377+
328378
void WalletView::unlockWallet(bool fForMixingOnly)
329379
{
330380
// Unlock wallet when requested by wallet model

src/qt/walletview.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ public Q_SLOTS:
107107
void backupWallet();
108108
/** Change encrypted wallet passphrase */
109109
void changePassphrase();
110+
/** Show wallet mnemonic/recovery phrase */
111+
void showMnemonic();
110112
/** Ask for passphrase to unlock wallet temporarily */
111113
void unlockWallet(bool fAnonymizeOnly=false);
112114
/** Lock wallet */

0 commit comments

Comments
 (0)