From 77b05a84f20845d54f0ea444e51392ce4f7e8178 Mon Sep 17 00:00:00 2001 From: Sami Date: Mon, 15 Jun 2026 14:44:33 -0700 Subject: [PATCH] wallet(HD): Qt UI - Seed Phrase dialog (generate/restore/backup) Adds HDSeedDialog (Settings > Seed Phrase) with Generate New / Reveal for Backup / Restore from Phrase, driven by new WalletModel HD methods. Restore rescans the chain. Requires wallet unlock via the standard UnlockContext. --- src/CMakeLists.txt | 1 + src/qt/hdseeddialog.cpp | 142 ++++++++++++++++++++++++++++++++++++++++ src/qt/hdseeddialog.h | 34 ++++++++++ src/qt/trianglesgui.cpp | 14 ++++ src/qt/trianglesgui.h | 3 + src/qt/walletmodel.cpp | 46 +++++++++++++ src/qt/walletmodel.h | 6 ++ 7 files changed, 246 insertions(+) create mode 100644 src/qt/hdseeddialog.cpp create mode 100644 src/qt/hdseeddialog.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 13865f2..9b5fd86 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -307,6 +307,7 @@ if(BUILD_QT) qt/trianglesunits.cpp qt/qvaluecombobox.cpp qt/askpassphrasedialog.cpp + qt/hdseeddialog.cpp qt/notificator.cpp qt/qtipcserver.cpp qt/rpcconsole.cpp diff --git a/src/qt/hdseeddialog.cpp b/src/qt/hdseeddialog.cpp new file mode 100644 index 0000000..5f89373 --- /dev/null +++ b/src/qt/hdseeddialog.cpp @@ -0,0 +1,142 @@ +// Copyright (c) 2026 The Triangles developers +// Distributed under the MIT/X11 software license. +#include "hdseeddialog.h" +#include "walletmodel.h" + +#include +#include +#include +#include +#include +#include +#include + +HDSeedDialog::HDSeedDialog(QWidget *parent) + : QDialog(parent), model(0), seedText(0), statusLabel(0) +{ + setWindowTitle(tr("HD Seed Phrase (BIP39)")); + resize(560, 360); + + QVBoxLayout *layout = new QVBoxLayout(this); + + QLabel *intro = new QLabel(tr( + "A 24-word seed phrase is a complete backup of this wallet. Anyone who has it " + "can spend your coins. Write it down on paper and keep it offline."), this); + intro->setWordWrap(true); + layout->addWidget(intro); + + seedText = new QPlainTextEdit(this); + seedText->setPlaceholderText(tr( + "Your 24-word phrase appears here when you generate or reveal it. " + "To restore, paste an existing 24-word phrase here and click 'Restore from Phrase'.")); + QFont mono("monospace"); + mono.setStyleHint(QFont::Monospace); + seedText->setFont(mono); + layout->addWidget(seedText); + + statusLabel = new QLabel(this); + statusLabel->setWordWrap(true); + layout->addWidget(statusLabel); + + QHBoxLayout *btns = new QHBoxLayout(); + QPushButton *genBtn = new QPushButton(tr("Generate New"), this); + QPushButton *showBtn = new QPushButton(tr("Reveal for Backup"), this); + QPushButton *restoreBtn = new QPushButton(tr("Restore from Phrase"), this); + QPushButton *closeBtn = new QPushButton(tr("Close"), this); + btns->addWidget(genBtn); + btns->addWidget(showBtn); + btns->addWidget(restoreBtn); + btns->addStretch(); + btns->addWidget(closeBtn); + layout->addLayout(btns); + + connect(genBtn, SIGNAL(clicked()), this, SLOT(onGenerate())); + connect(showBtn, SIGNAL(clicked()), this, SLOT(onShow())); + connect(restoreBtn, SIGNAL(clicked()), this, SLOT(onRestore())); + connect(closeBtn, SIGNAL(clicked()), this, SLOT(accept())); +} + +void HDSeedDialog::setModel(WalletModel *modelIn) +{ + model = modelIn; + refreshStatus(); +} + +void HDSeedDialog::refreshStatus() +{ + if (!model || !statusLabel) return; + if (model->hdEnabled()) + statusLabel->setText(tr("Status: HD seed is ACTIVE. Use 'Reveal for Backup' to view your phrase.")); + else + statusLabel->setText(tr("Status: no HD seed yet. Use 'Generate New' to create one.")); +} + +void HDSeedDialog::onGenerate() +{ + if (!model) return; + if (model->hdEnabled()) { + QMessageBox::warning(this, tr("HD seed already set"), + tr("This wallet already has an HD seed. Use 'Reveal for Backup' to view it.")); + return; + } + if (QMessageBox::question(this, tr("Generate new seed"), + tr("Generate a new 24-word HD seed for this wallet?"), + QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) + return; + + WalletModel::UnlockContext ctx(model->requestUnlock()); + if (!ctx.isValid()) return; + + QString mnemonic, err; + if (!model->hdNew(mnemonic, err)) { + QMessageBox::critical(this, tr("Error"), err); + return; + } + seedText->setPlainText(mnemonic); + QMessageBox::information(this, tr("Write this down"), + tr("Your new 24-word seed phrase is shown above. Write it on paper and store it safely " + "and offline. This is the only backup of this wallet.")); + refreshStatus(); +} + +void HDSeedDialog::onShow() +{ + if (!model) return; + WalletModel::UnlockContext ctx(model->requestUnlock()); + if (!ctx.isValid()) return; + + QString mnemonic, err; + if (!model->hdShow(mnemonic, err)) { + QMessageBox::critical(this, tr("Error"), err); + return; + } + seedText->setPlainText(mnemonic); +} + +void HDSeedDialog::onRestore() +{ + if (!model) return; + QString phrase = seedText->toPlainText().trimmed(); + if (phrase.isEmpty()) { + QMessageBox::warning(this, tr("No phrase"), + tr("Paste a 24-word phrase into the box first.")); + return; + } + if (QMessageBox::question(this, tr("Restore from phrase"), + tr("Restore the HD seed from the phrase in the box and rescan the chain? " + "This replaces the current HD seed."), + QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) + return; + + WalletModel::UnlockContext ctx(model->requestUnlock()); + if (!ctx.isValid()) return; + + QString err; + if (!model->hdRestore(phrase, err)) { + QMessageBox::critical(this, tr("Error"), err); + return; + } + QMessageBox::information(this, tr("Restored"), + tr("HD seed restored and the chain was rescanned for your funds.")); + refreshStatus(); +} diff --git a/src/qt/hdseeddialog.h b/src/qt/hdseeddialog.h new file mode 100644 index 0000000..51ba558 --- /dev/null +++ b/src/qt/hdseeddialog.h @@ -0,0 +1,34 @@ +// Copyright (c) 2026 The Triangles developers +// Distributed under the MIT/X11 software license. +#ifndef HDSEEDDIALOG_H +#define HDSEEDDIALOG_H + +#include + +class WalletModel; +QT_BEGIN_NAMESPACE +class QPlainTextEdit; +class QLabel; +QT_END_NAMESPACE + +/** Generate, reveal (for backup), and restore the wallet's BIP39 HD seed phrase. */ +class HDSeedDialog : public QDialog +{ + Q_OBJECT +public: + explicit HDSeedDialog(QWidget *parent = 0); + void setModel(WalletModel *model); + +private: + WalletModel *model; + QPlainTextEdit *seedText; + QLabel *statusLabel; + void refreshStatus(); + +private slots: + void onGenerate(); + void onShow(); + void onRestore(); +}; + +#endif // HDSEEDDIALOG_H diff --git a/src/qt/trianglesgui.cpp b/src/qt/trianglesgui.cpp index f042f49..66dbfaf 100644 --- a/src/qt/trianglesgui.cpp +++ b/src/qt/trianglesgui.cpp @@ -31,6 +31,7 @@ #include "trianglesunits.h" #include "guiconstants.h" #include "askpassphrasedialog.h" +#include "hdseeddialog.h" #include "notificator.h" #include "guiutil.h" #include "rpcconsole.h" @@ -484,6 +485,8 @@ void TrianglesGUI::createActions(bool fIsTestnet) backupWalletAction->setStatusTip(tr("Backup wallet to another location")); changePassphraseAction = new QAction(QIcon(":/menu_16/passphrase"), tr("&Change Passphrase..."), this); changePassphraseAction->setStatusTip(tr("Change the passphrase used for wallet encryption")); + hdSeedAction = new QAction(QIcon(":/menu_16/passphrase"), tr("&Seed Phrase (HD Backup)..."), this); + hdSeedAction->setStatusTip(tr("Generate, restore, or back up your 24-word HD seed phrase")); unlockWalletAction = new QAction(QIcon(":/menu_16/unlock"), tr("&Unlock Wallet..."), this); unlockWalletAction->setStatusTip(tr("Unlock wallet")); unlockWalletStakingAction = new QAction(QIcon(":/menu_16/unlock"), tr("&Unlock Wallet for staking..."), this); @@ -509,6 +512,7 @@ void TrianglesGUI::createActions(bool fIsTestnet) connect(encryptWalletAction, SIGNAL(triggered(bool)), this, SLOT(encryptWallet(bool))); connect(backupWalletAction, SIGNAL(triggered()), this, SLOT(backupWallet())); connect(changePassphraseAction, SIGNAL(triggered()), this, SLOT(changePassphrase())); + connect(hdSeedAction, SIGNAL(triggered()), this, SLOT(hdSeedManager())); connect(unlockWalletAction, SIGNAL(triggered()), this, SLOT(unlockWallet())); connect(unlockWalletStakingAction, SIGNAL(triggered()), this, SLOT(unlockWalletStaking())); connect(lockWalletAction, SIGNAL(triggered()), this, SLOT(lockWallet())); @@ -538,6 +542,7 @@ void TrianglesGUI::createMenuBar() QMenu *settings = appMenuBar->addMenu(tr("&Settings")); settings->addAction(encryptWalletAction); settings->addAction(changePassphraseAction); + settings->addAction(hdSeedAction); settings->addAction(unlockWalletAction); settings->addAction(lockWalletAction); settings->addSeparator(); @@ -1614,6 +1619,15 @@ void TrianglesGUI::changePassphrase() dlg.exec(); } +void TrianglesGUI::hdSeedManager() +{ + if (!walletModel) + return; + HDSeedDialog dlg(this); + dlg.setModel(walletModel); + dlg.exec(); +} + void TrianglesGUI::unlockWalletStaking() { diff --git a/src/qt/trianglesgui.h b/src/qt/trianglesgui.h index 7d8ec64..8f61e0e 100644 --- a/src/qt/trianglesgui.h +++ b/src/qt/trianglesgui.h @@ -131,6 +131,7 @@ private: QAction *encryptWalletAction; QAction *backupWalletAction; QAction *changePassphraseAction; + QAction *hdSeedAction; QAction *unlockWalletAction; QAction *unlockWalletStakingAction; QAction *lockWalletAction; @@ -243,6 +244,8 @@ private slots: void backupWallet(); /** Change encrypted wallet passphrase */ void changePassphrase(); + /** Open the HD seed phrase (generate/restore/backup) dialog */ + void hdSeedManager(); /** Ask for passphrase to unlock wallet temporarily */ void unlockWallet(); /** Ask for passphrase to unlock wallet temporarily - FOR STAKING ONLY */ diff --git a/src/qt/walletmodel.cpp b/src/qt/walletmodel.cpp index 810c6ea..7054532 100644 --- a/src/qt/walletmodel.cpp +++ b/src/qt/walletmodel.cpp @@ -676,3 +676,49 @@ void WalletModel::listLockedCoins(std::vector& vOutpts) { return; } + + +// ---- HD wallet (BIP39/BIP32) ---- +bool WalletModel::hdEnabled() const +{ + return wallet->IsHDEnabled(); +} + +bool WalletModel::hdNew(QString &mnemonicOut, QString &errorOut) +{ + std::string mnemonic, strError; + if (!wallet->SetHDSeed("", "", true, mnemonic, strError)) { + errorOut = QString::fromStdString(strError); + return false; + } + wallet->TopUpKeyPool(); + mnemonicOut = QString::fromStdString(mnemonic); + return true; +} + +bool WalletModel::hdRestore(const QString &mnemonic, QString &errorOut) +{ + std::string out, strError; + if (!wallet->SetHDSeed(mnemonic.toStdString(), "", false, out, strError)) { + errorOut = QString::fromStdString(strError); + return false; + } + wallet->TopUpKeyPool(); + { + LOCK2(cs_main, wallet->cs_wallet); + wallet->ScanForWalletTransactions(pindexGenesisBlock, true); + wallet->ReacceptWalletTransactions(); + } + return true; +} + +bool WalletModel::hdShow(QString &mnemonicOut, QString &errorOut) +{ + std::string mnemonic; + if (!wallet->GetHDMnemonic(mnemonic)) { + errorOut = QObject::tr("Wallet has no HD seed (use 'Generate New')."); + return false; + } + mnemonicOut = QString::fromStdString(mnemonic); + return true; +} diff --git a/src/qt/walletmodel.h b/src/qt/walletmodel.h index 9e588de..ac6bf59 100644 --- a/src/qt/walletmodel.h +++ b/src/qt/walletmodel.h @@ -103,6 +103,12 @@ public: // Wallet backup bool backupWallet(const QString &filename); + // ---- HD wallet (BIP39/BIP32) ---- + bool hdEnabled() const; + bool hdNew(QString &mnemonicOut, QString &errorOut); + bool hdRestore(const QString &mnemonic, QString &errorOut); + bool hdShow(QString &mnemonicOut, QString &errorOut); + // RAI object for unlocking wallet, returned by requestUnlock() class UnlockContext {