diff --git a/CMakeLists.txt b/CMakeLists.txt index 3098ddf..08d8229 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.18) project(feather - VERSION "2.6.5" + VERSION "2.6.7" DESCRIPTION "A free Monero desktop wallet" LANGUAGES CXX C ASM ) @@ -284,10 +284,13 @@ if (WIN32) endif() if(STATIC) -# add_linker_flag_if_supported(-static-libgcc STATIC_FLAGS) -# add_linker_flag_if_supported(-static-libstdc++ STATIC_FLAGS) if(MINGW) add_linker_flag_if_supported(-static STATIC_FLAGS) + elseif (NOT (APPLE OR FREEBSD OR OPENBSD OR DRAGONFLY)) + if(NOT "${ARCH}" STREQUAL "armv7-a") + add_linker_flag_if_supported(-static-libgcc STATIC_FLAGS) + add_linker_flag_if_supported(-static-libstdc++ STATIC_FLAGS) + endif() endif() endif() diff --git a/monero b/monero index 892a9a1..5458a8a 160000 --- a/monero +++ b/monero @@ -1 +1 @@ -Subproject commit 892a9a16d169ecd8f15e892f1e14fdd84cfce0f3 +Subproject commit 5458a8a6630fa4c75e9c1df9e2828739afa88124 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ae06ddb..400df99 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -20,8 +20,8 @@ endif() find_package(Qt6 REQUIRED COMPONENTS ${QT_COMPONENTS}) -set(QAPPLICATION_CLASS QApplication CACHE STRING "Inheritance class for SingleApplication") -add_subdirectory(third-party/singleapplication) +#set(QAPPLICATION_CLASS QApplication CACHE STRING "Inheritance class for SingleApplication") +#add_subdirectory(third-party/singleapplication) if (CHECK_UPDATES) add_subdirectory(openpgp) @@ -55,6 +55,8 @@ file(GLOB SOURCE_FILES "widgets/*.cpp" "wizard/*.h" "wizard/*.cpp" + "wizard/multisig/*.h" + "wizard/multisig/*.cpp" "wallet/*.h" "wallet/*.cpp" "qrcode/*.h" @@ -272,7 +274,7 @@ target_link_libraries(feather PRIVATE Threads::Threads ${QRENCODE_LIBRARY} ${POLYSEED_LIBRARY} - SingleApplication::SingleApplication +# SingleApplication::SingleApplication ${ICU_LIBRARIES} ${LIBZIP_LIBRARIES} ${ZLIB_LIBRARIES} diff --git a/src/CoinsWidget.cpp b/src/CoinsWidget.cpp index 84715d2..11baae5 100644 --- a/src/CoinsWidget.cpp +++ b/src/CoinsWidget.cpp @@ -89,11 +89,8 @@ void CoinsWidget::setModel(CoinsModel * model, Coins * coins) { ui->coins->setColumnHidden(CoinsModel::SpentHeight, true); ui->coins->setColumnHidden(CoinsModel::Frozen, true); - if (!m_wallet->viewOnly()) { - ui->coins->setColumnHidden(CoinsModel::KeyImageKnown, true); - } else { - ui->coins->setColumnHidden(CoinsModel::KeyImageKnown, false); - } + bool showKeyImageKnown = m_wallet->viewOnly() || m_wallet->isMultisig(); + ui->coins->setColumnHidden(CoinsModel::KeyImageKnown, !showKeyImageKnown); ui->coins->header()->setSectionResizeMode(QHeaderView::ResizeToContents); ui->coins->header()->setSectionResizeMode(CoinsModel::Label, QHeaderView::Stretch); @@ -335,6 +332,11 @@ bool CoinsWidget::isCoinSpendable(CoinsInfo *coin) { return false; } + if (!coin->haveMultisigK()) { + Utils::showError(this, "Unable to spend outputs", "We already spent this output in another transaction proposal, other participants would not be able to sign both transactions"); + return false; + } + if (coin->spent()) { Utils::showError(this, "Unable to spend outputs", "Selected output was already spent"); return false; diff --git a/src/MMSWidget.cpp b/src/MMSWidget.cpp new file mode 100644 index 0000000..08cf489 --- /dev/null +++ b/src/MMSWidget.cpp @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "MMSWidget.h" +#include "ui_MMSWidget.h" + +#include +#include + +#include "dialog/OutputInfoDialog.h" +#include "dialog/OutputSweepDialog.h" +#include "utils/Icons.h" +#include "utils/Utils.h" + +MMSWidget::MMSWidget(Wallet *wallet, QWidget *parent) + : QWidget(parent) + , ui(new Ui::MMSWidget) + , m_wallet(wallet) +{ + ui->setupUi(this); + + connect(ui->btn_mmsNext, &QPushButton::clicked, [this]{ + m_store->next(); + }); + + connect(ui->btn_mmsNextSync, &QPushButton::clicked, [this]{ + m_store->next(true); + }); + + connect(ui->btn_exportMultisig, &QPushButton::clicked, [this]{ + m_store->exportMultisig(); + }); + + connect(ui->btn_deleteAll, &QPushButton::clicked, [this]{ + m_store->deleteAllMessages(); + }); + + ui->mms->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->mms, &QTreeView::customContextMenuRequested, this, &MMSWidget::showContextMenu); + connect(ui->transactionProposals, &QTreeView::customContextMenuRequested, this, &MMSWidget::showContexxtMenu); + + connect(m_wallet->mmsStore(), &MultisigMessageStore::connectionError, [this]{ + Utils::showError(this, "Unable to connect to message service"); + }); + +// ui->tabWidget->setTabVisible(0, false); +} + +void MMSWidget::setModel(MultisigMessageModel * model, MultisigIncomingTxModel * incomingModel, MultisigMessageStore * store) { + m_store = store; + m_model = model; + ui->mms->setModel(m_model); + + ui->mms->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + ui->mms->setSortingEnabled(true); + + ui->mms->setColumnHidden(MultisigMessageModel::Modified, true); + ui->mms->setColumnHidden(MultisigMessageModel::Sent, true); + ui->mms->setColumnHidden(MultisigMessageModel::Hash, true); + ui->mms->setColumnHidden(MultisigMessageModel::TransportId, true); + ui->mms->setColumnHidden(MultisigMessageModel::WalletHeight, true); + ui->mms->setColumnHidden(MultisigMessageModel::SignatureCount, true); + + ui->transactionProposals->setModel(incomingModel); + ui->transactionProposals->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + ui->transactionProposals->setSortingEnabled(true); + ui->transactionProposals->setColumnHidden(MultisigIncomingTxModel::MessageID, true); + ui->transactionProposals->setColumnHidden(MultisigIncomingTxModel::TxCount, true); + ui->transactionProposals->setColumnHidden(MultisigIncomingTxModel::TxId, true); + ui->transactionProposals->setColumnHidden(MultisigIncomingTxModel::Signatures, true); +} + +void MMSWidget::showContextMenu(const QPoint &point) { + auto* menu = new QMenu(this); + + menu->addAction("Sign transaction", [this, point]{ + quint32 id = this->idAtPoint(ui->mms, point); + m_store->signTx(id); + }); + + menu->addAction("Delete message", [this, point]{ + quint32 id = this->idAtPoint(ui->mms, point); + m_store->deleteMessage(id); + }); + + menu->addAction("Copy message", [this, point]{ + quint32 id = this->idAtPoint(ui->mms, point); + std::string content = m_store->exportMessage(id); + Utils::copyToClipboard(QString::fromStdString(content)); + }); + + menu->addAction("Export message", [this, point]{ + quint32 id = this->idAtPoint(ui->mms, point); + std::string content = m_store->exportMessage(id); + + QString defaultName = QString("mms_message_content_%1.mms").arg(QString::number(id)); + QString fn = Utils::getSaveFileName(this, "Save key images to file", defaultName, "Key Images (*_keyImages)"); + if (fn.isEmpty()) { + return; + } + + QFile file{fn}; + if (!file.open(QIODevice::WriteOnly)) { + Utils::showError(this, "Failed to export MMS message", QString("Could not open file %1 for writing").arg(fn)); + return; + } + + file.write(content.data(), content.size()); + file.close(); + }); + + menu->popup(ui->mms->viewport()->mapToGlobal(point)); +} + +void MMSWidget::showContexxtMenu(const QPoint &point) { + auto* menu = new QMenu(this); + + menu->addAction("Review transaction", [this, point]{ + quint32 id = iddAtPoint(point); + m_store->signTx(id); + }); + + menu->addAction("Remove transaction", [this, point]{ + quint32 id = iddAtPoint(point); + m_store->deleteMessage(id); + }); + + menu->popup(ui->transactionProposals->viewport()->mapToGlobal(point)); +} + +quint32 MMSWidget::iddAtPoint(const QPoint &point) { + QModelIndex index = ui->transactionProposals->indexAt(point); + if (!index.isValid()) { + return 0; + } + + QModelIndex dataIndex = index.sibling(index.row(), MultisigIncomingTxModel::MessageID); + + return dataIndex.data().toUInt(); +} + +quint32 MMSWidget::idAtPoint(QTreeView *tree, const QPoint &point) { + QModelIndex index = tree->indexAt(point); + if (!index.isValid()) { + return 0; + } + + QModelIndex dataIndex = index.sibling(index.row(), MultisigMessageModel::Id); + return dataIndex.data().toUInt(); +} + +MMSWidget::~MMSWidget() = default; \ No newline at end of file diff --git a/src/MMSWidget.h b/src/MMSWidget.h new file mode 100644 index 0000000..f357beb --- /dev/null +++ b/src/MMSWidget.h @@ -0,0 +1,44 @@ +// +// Created by user on 1/3/24. +// + +#ifndef FEATHER_MMSWIDGET_H +#define FEATHER_MMSWIDGET_H + +#include +#include +#include +#include + +#include "model/MultisigMessageModel.h" +#include "model/MultisigIncomingTxModel.h" +#include "libwalletqt/MultisigMessageStore.h" +#include "libwalletqt/Wallet.h" + +namespace Ui { + class MMSWidget; +} + +class MMSWidget : public QWidget +{ +Q_OBJECT + +public: + explicit MMSWidget(Wallet *wallet, QWidget *parent = nullptr); + void setModel(MultisigMessageModel *model, MultisigIncomingTxModel *incomingTxModel, MultisigMessageStore *store); + ~MMSWidget() override; + +private: + quint32 idAtPoint(QTreeView *tree, const QPoint &point); + quint32 iddAtPoint(const QPoint &point); + void showContextMenu(const QPoint &point); + void showContexxtMenu(const QPoint &point); + + QScopedPointer ui; + Wallet *m_wallet; + + MultisigMessageStore *m_store; + MultisigMessageModel * m_model; +}; + +#endif //FEATHER_MMSWIDGET_H diff --git a/src/MMSWidget.ui b/src/MMSWidget.ui new file mode 100644 index 0000000..7753ddd --- /dev/null +++ b/src/MMSWidget.ui @@ -0,0 +1,112 @@ + + + MMSWidget + + + + 0 + 0 + 857 + 464 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + Transaction proposals + + + + + + Qt::CustomContextMenu + + + false + + + + + + + + Message store + + + + + + + + + + + Next + + + + + + + Next (force sync) + + + + + + + Export multisig + + + + + + + Delete all + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 14b7db8..ecf340c 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -239,6 +239,10 @@ void MainWindow::initWidgets() { m_coinsWidget = new CoinsWidget(m_wallet, this); ui->coinsWidgetLayout->addWidget(m_coinsWidget); + // [MMS] + m_mmsWidget = new MMSWidget(m_wallet, this); + ui->mmsWidgetLayout->addWidget(m_mmsWidget); + // [Plugins..] for (auto* plugin : m_plugins) { if (!plugin->hasParent()) { @@ -485,6 +489,7 @@ void MainWindow::initWalletContext() { }); connect(m_wallet, &Wallet::multiBroadcast, this, &MainWindow::onMultiBroadcast); + connect(m_wallet->mmsStore(), &MultisigMessageStore::askToSign, this, &MainWindow::onAskToSign); } void MainWindow::menuToggleTabVisible(const QString &key){ @@ -523,6 +528,8 @@ QString MainWindow::walletKeysPath() { } void MainWindow::onWalletOpened() { + qDebug() << this->thread(); + qDebug() << Q_FUNC_INFO; m_splashDialog->hide(); @@ -553,6 +560,13 @@ void MainWindow::onWalletOpened() { m_coinsWidget->setModel(m_wallet->coinsModel(), m_wallet->coins()); m_wallet->coinsModel()->setCurrentSubaddressAccount(m_wallet->currentSubaddressAccount()); + // mms page + if (m_wallet->isMultisig()) { + m_wallet->mmsStore()->refresh(); + m_mmsWidget->setModel(m_wallet->mmsModel(), m_wallet->msIncomingTxModel(), m_wallet->mmsStore()); + m_wallet->setMMSRefreshEnabled(true); + } + // Coin labeling uses set_tx_note, so we need to refresh history too connect(m_wallet->coins(), &Coins::descriptionChanged, [this] { m_wallet->history()->refresh(); @@ -695,6 +709,47 @@ void MainWindow::onMultiBroadcast(const QMap &txHexMap) { } } +void MainWindow::onAskToSign(PendingTransaction *tx) { + QString address = tx->destinationAddresses(0)[0]; + + TxConfAdvDialog dialog_adv{m_wallet, m_wallet->tmpTxDescription, this}; + dialog_adv.setMultisigTransaction(tx); + dialog_adv.exec(); + + // TODO: UNSAFE + m_wallet->mmsStore()->refresh(false); + + return; + + TxConfDialog dialog{m_wallet, tx, address, m_wallet->tmpTxDescription, this}; + switch (dialog.exec()) { + case QDialog::Rejected: + { + if (!dialog.showAdvanced) { + m_wallet->disposeTransaction(tx); + } + break; + } + case QDialog::Accepted: + { + tx->signMultisigTx(); + if (tx->status() != PendingTransaction::Status_Ok) { + Utils::showError(this, "Unable to sign multisig transaction", tx->errorString()); + return; + } + + m_wallet->commitTransaction(tx, m_wallet->tmpTxDescription); + break; + } + } + + if (dialog.showAdvanced) { + TxConfAdvDialog dialog_adv{m_wallet, m_wallet->tmpTxDescription, this}; + dialog_adv.setTransaction(tx); + dialog_adv.exec(); + } +} + void MainWindow::onSyncStatus(quint64 height, quint64 target, bool daemonSync) { if (height >= (target - 1)) { this->updateNetStats(); @@ -957,8 +1012,17 @@ void MainWindow::onTransactionCreated(PendingTransaction *tx, const QVectorcommitTransaction(tx, m_wallet->tmpTxDescription); + { + if (m_wallet->isMultisig() && m_wallet->multisigThreshold() > 1) { + quint32 id = tx->saveToMMS(); + m_wallet->mmsStore()->sendPendingTransaction(id, dialog.getMultisigSignerIndex()); + m_wallet->coins()->refresh(); + m_sendWidget->clearFields(); + } else { + m_wallet->commitTransaction(tx, m_wallet->tmpTxDescription); + } break; + } } if (dialog.showAdvanced) { @@ -1037,7 +1101,7 @@ void MainWindow::showSeedDialog() { return; } - if (!m_wallet->isDeterministic()) { + if (!m_wallet->isDeterministic() && !m_wallet->isMultisig()) { Utils::showInfo(this, "Seed unavailable", "Wallet is non-deterministic and has no seed", {"To obtain wallet keys go to Wallet -> Keys"}, "show_wallet_seed"); return; @@ -1047,6 +1111,12 @@ void MainWindow::showSeedDialog() { return; } + if (m_wallet->isMultisig() && m_wallet->hasMultisigPartialKeyImages()) { + Utils::copyToClipboard(m_wallet->getMultisigSeed()); + Utils::showInfo(this, "Multisig seed copied to clipboard"); + return; + } + SeedDialog dialog{m_wallet, this}; dialog.exec(); } @@ -1682,6 +1752,10 @@ void MainWindow::updateTitle() { title += " [view-only]"; } + if (m_wallet->isMultisig()) { + title += " [multisig]"; + } + title += " - Feather"; this->setWindowTitle(title); diff --git a/src/MainWindow.h b/src/MainWindow.h index 4b2ceb1..3d31ea2 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -38,6 +38,7 @@ #include "SendWidget.h" #include "ReceiveWidget.h" #include "CoinsWidget.h" +#include "MMSWidget.h" #include "WindowManager.h" #include "plugins/Plugin.h" @@ -165,6 +166,7 @@ private slots: void onProxySettingsChanged(); void onOfflineMode(bool offline); void onMultiBroadcast(const QMap &txHexMap); + void onAskToSign(PendingTransaction *tx); private: friend WindowManager; @@ -220,6 +222,7 @@ private: SendWidget *m_sendWidget = nullptr; ReceiveWidget *m_receiveWidget = nullptr; CoinsWidget *m_coinsWidget = nullptr; + MMSWidget *m_mmsWidget = nullptr; QPointer m_clearRecentlyOpenAction; diff --git a/src/MainWindow.ui b/src/MainWindow.ui index e9b390e..da73369 100644 --- a/src/MainWindow.ui +++ b/src/MainWindow.ui @@ -59,7 +59,7 @@ - 0 + 4 @@ -158,6 +158,20 @@ + + + + :/assets/images/sign.png:/assets/images/sign.png + + + Multisig + + + + + + + diff --git a/src/SendWidget.cpp b/src/SendWidget.cpp index 8986600..80216dc 100644 --- a/src/SendWidget.cpp +++ b/src/SendWidget.cpp @@ -243,6 +243,9 @@ void SendWidget::sendClicked() { #endif } + // TODO: force key image sync here for multisig wallets? +// if (m_wallet->isMultisig() && m_wallet->mul) + m_wallet->createTransaction(recipient, amount, description, sendAll); } diff --git a/src/WindowManager.cpp b/src/WindowManager.cpp index 6cdf92a..8f11e74 100644 --- a/src/WindowManager.cpp +++ b/src/WindowManager.cpp @@ -26,7 +26,7 @@ WindowManager::WindowManager(QObject *parent) m_splashDialog = new SplashDialog(); m_cleanupThread = new QThread(this); - connect(m_walletManager, &WalletManager::walletOpened, this, &WindowManager::onWalletOpened); + connect(m_walletManager, &WalletManager::walletOpened, this, &WindowManager::onWalletOpened); connect(m_walletManager, &WalletManager::walletCreated, this, &WindowManager::onWalletCreated); connect(m_walletManager, &WalletManager::deviceButtonRequest, this, &WindowManager::onDeviceButtonRequest); connect(m_walletManager, &WalletManager::deviceButtonPressed, this, &WindowManager::onDeviceButtonPressed); @@ -75,6 +75,16 @@ void WindowManager::quitAfterLastWindow() { return; } + if (m_openingWallet) { + qDebug() << "We're openening a wallet, don't quit application"; + return; + } + + if (m_wizard && m_wizard->isVisible()) { + qDebug() << "This wizard is still open, don't quit application"; + return; + } + qDebug() << "No wizards in progress and no wallets open, quitting application."; this->close(); } @@ -85,10 +95,18 @@ void WindowManager::close() { window->close(); } - m_wizard->deleteLater(); - m_splashDialog->deleteLater(); - m_tray->deleteLater(); - m_docsDialog->deleteLater(); + if (m_splashDialog) { + m_splashDialog->deleteLater(); + } + if (m_tray) { + m_tray->deleteLater(); + } + if (m_wizard) { + m_wizard->deleteLater(); + } + if (m_docsDialog) { + m_docsDialog->deleteLater(); + } torManager()->stop(); @@ -296,10 +314,28 @@ void WindowManager::onWalletOpened(Wallet *wallet) { m_splashDialog->hide(); m_openWalletTriedOnce = false; + + QString multisigSetup = wallet->getCacheAttribute("feather.multisig_setup"); + if (multisigSetup == "started") { + qDebug() << "Multisig setup in progress, but not finished."; + m_openingWallet = false; + this->showWizard(WalletWizard::Page_MultisigCreateSetupKey, wallet); + return; + } + else if (multisigSetup == "configured") { + qDebug() << "Multisig setup configured, but not completed"; + m_openingWallet = false; + this->showWizard(WalletWizard::Page_MultisigSetupWallet, wallet); + return; + } + + qDebug() << "creating new mainwindow"; auto *window = new MainWindow(this, wallet); m_windows.append(window); - this->buildTrayMenu(); + m_openingWallet = false; + + this->buildTrayMenu(); } void WindowManager::onWalletOpenPasswordRequired(bool invalidPassword, const QString &path) { @@ -338,7 +374,7 @@ bool WindowManager::autoOpenWallet() { // ######################## WALLET CREATION ######################## void WindowManager::tryCreateWallet(Seed seed, const QString &path, const QString &password, const QString &seedLanguage, - const QString &seedOffset, const QString &subaddressLookahead, bool newWallet) { + const QString &seedOffset, const QString &subaddressLookahead, bool newWallet, bool giveToWizard) { if (Utils::fileExists(path)) { this->handleWalletError({nullptr, Utils::ERROR, "Failed to create wallet", QString("File already exists: %1").arg(path)}); return; @@ -366,7 +402,14 @@ void WindowManager::tryCreateWallet(Seed seed, const QString &path, const QStrin wallet->setCacheAttribute("feather.seedoffset", seedOffset); if (newWallet) { - wallet->setNewWallet(); + wallet->setNewWallet(); + } + + if (giveToWizard) { + qDebug() << "Giving wallet to wizard instead of opening"; + m_wizard->setWallet(wallet); + m_openingWallet = false; + return; } this->onWalletOpened(wallet); @@ -417,6 +460,26 @@ void WindowManager::tryCreateWalletFromKeys(const QString &path, const QString & this->onWalletOpened(wallet); } +void WindowManager::tryRestoreMultisigWallet(const QString &path, const QString &password, const QString &multisigSeed, + const QString &mmsRecovery, quint64 restoreHeight, const QString &subaddressLookahead) +{ + if (Utils::fileExists(path)) { + this->handleWalletError({nullptr, Utils::ERROR, "Failed to create wallet", QString("File already exists: %1").arg(path)}); + return; + } + + if (multisigSeed.isEmpty()) { + this->handleWalletError({nullptr, Utils::ERROR, "Failed to create wallet", "Multisig seed is empty"}); + return; + } + + Wallet *wallet; + wallet = m_walletManager->restoreMultisigWallet(path, password, constants::networkType, multisigSeed, mmsRecovery, restoreHeight, constants::kdfRounds, subaddressLookahead); + + m_openingWallet = true; + this->onWalletOpened(wallet); +} + void WindowManager::onWalletCreated(Wallet *wallet) { // Currently only called when a wallet is created from device. auto state = wallet->status(); @@ -686,6 +749,8 @@ WalletWizard* WindowManager::createWizard(WalletWizard::Page startPage) { connect(wizard, &WalletWizard::createWallet, this, &WindowManager::tryCreateWallet); connect(wizard, &WalletWizard::createWalletFromKeys, this, &WindowManager::tryCreateWalletFromKeys); connect(wizard, &WalletWizard::createWalletFromDevice, this, &WindowManager::tryCreateWalletFromDevice); + connect(wizard, &WalletWizard::restoreMultisigWallet, this, &WindowManager::tryRestoreMultisigWallet); + connect(wizard, &WalletWizard::showWallet, [this](Wallet *wallet){this->onWalletOpened(wallet);}); return wizard; } @@ -698,12 +763,17 @@ void WindowManager::initWizard() { this->showWizard(startPage); } -void WindowManager::showWizard(WalletWizard::Page startPage) { +void WindowManager::showWizard(WalletWizard::Page startPage, Wallet *wallet) { if (!m_wizard) { m_wizard = this->createWizard(startPage); } m_wizard->resetFields(); + + if (wallet) { + m_wizard->setWallet(wallet); + } + m_wizard->setStartId(startPage); m_wizard->restart(); m_wizard->setEnabled(true); diff --git a/src/WindowManager.h b/src/WindowManager.h index 8df04c5..8966289 100644 --- a/src/WindowManager.h +++ b/src/WindowManager.h @@ -28,7 +28,7 @@ public: void wizardOpenWallet(); void close(); void closeWindow(MainWindow *window); - void showWizard(WalletWizard::Page startPage); + void showWizard(WalletWizard::Page startPage, Wallet *wallet = nullptr); void restartApplication(const QString &binaryFilename); void raise(); @@ -68,9 +68,10 @@ private slots: void onChangeTheme(const QString &themeName); private: - void tryCreateWallet(Seed seed, const QString &path, const QString &password, const QString &seedLanguage, const QString &seedOffset, const QString &subaddressLookahead, bool newWallet); + void tryCreateWallet(Seed seed, const QString &path, const QString &password, const QString &seedLanguage, const QString &seedOffset, const QString &subaddressLookahead, bool newWallet, bool giveToWizard = false); void tryCreateWalletFromDevice(const QString &path, const QString &password, const QString &deviceName, int restoreHeight, const QString &subaddressLookahead); void tryCreateWalletFromKeys(const QString &path, const QString &password, const QString &address, const QString &viewkey, const QString &spendkey, quint64 restoreHeight, const QString &subaddressLookahead); + void tryRestoreMultisigWallet(const QString &path, const QString &password, const QString &multisigSeed, const QString &mmsRecovery, quint64 restoreHeight, const QString &subaddressLookahead); bool autoOpenWallet(); @@ -107,6 +108,7 @@ private: bool m_openWalletTriedOnce = false; bool m_openingWallet = false; bool m_initialNetworkConfigured = false; + bool m_aboutToShowWizard = false; QThread *m_cleanupThread; }; diff --git a/src/assets.qrc b/src/assets.qrc index e84bf88..69de8d5 100644 --- a/src/assets.qrc +++ b/src/assets.qrc @@ -39,6 +39,7 @@ assets/images/eye_blind.png assets/images/file.png assets/images/file_manager_32px.png + assets/images/freeze.png assets/images/gnome-calc.png assets/images/hd_32px.png assets/images/history.png @@ -58,6 +59,7 @@ assets/images/localMonero_logo.png assets/images/localMonero_register.svg assets/images/lock.svg + assets/images/mail.png assets/images/microphone.png assets/images/mining.png assets/images/network.png diff --git a/src/assets/images/freeze.png b/src/assets/images/freeze.png new file mode 100644 index 0000000..8e28cc2 Binary files /dev/null and b/src/assets/images/freeze.png differ diff --git a/src/assets/images/mail.png b/src/assets/images/mail.png new file mode 100644 index 0000000..98c0945 Binary files /dev/null and b/src/assets/images/mail.png differ diff --git a/src/dialog/AccountSwitcherDialog.cpp b/src/dialog/AccountSwitcherDialog.cpp index 4c3a6a6..fcef9d4 100644 --- a/src/dialog/AccountSwitcherDialog.cpp +++ b/src/dialog/AccountSwitcherDialog.cpp @@ -50,7 +50,7 @@ AccountSwitcherDialog::AccountSwitcherDialog(Wallet *wallet, QWidget *parent) connect(m_wallet->subaddressAccount(), &SubaddressAccount::refreshFinished, this, &AccountSwitcherDialog::updateSelection); this->update(); - this->updateSelection(); +// this->updateSelection(); } void AccountSwitcherDialog::update() { diff --git a/src/dialog/SeedDialog.cpp b/src/dialog/SeedDialog.cpp index d54a186..146b255 100644 --- a/src/dialog/SeedDialog.cpp +++ b/src/dialog/SeedDialog.cpp @@ -21,28 +21,35 @@ SeedDialog::SeedDialog(Wallet *wallet, QWidget *parent) m_wallet->setSeedLanguage(constants::seedLanguage); } - QString seedOffset = m_wallet->getCacheAttribute("feather.seedoffset"); - QString seed = m_wallet->getCacheAttribute("feather.seed"); - auto seedLength = m_wallet->seedLength(); - - QString seed_25_words = m_wallet->getSeed(seedOffset); - - if (seedLength >= 24) { + if (m_wallet->isMultisig()) { + this->setMultisigSeed(m_wallet->getMultisigSeed()); + ui->frameSeedOffset->setVisible(false); ui->check_toggleSeedType->hide(); - this->setSeed(seed_25_words); - } else { - this->setSeed(seed); ui->frameRestoreHeight->setVisible(false); + } else { + QString seedOffset = m_wallet->getCacheAttribute("feather.seedoffset"); + QString seed = m_wallet->getCacheAttribute("feather.seed"); + auto seedLength = m_wallet->seedLength(); + + QString seed_25_words = m_wallet->getSeed(seedOffset); + + if (seedLength >= 24) { + ui->check_toggleSeedType->hide(); + this->setSeed(seed_25_words); + } else { + this->setSeed(seed); + ui->frameRestoreHeight->setVisible(false); + } + + ui->frameSeedOffset->setVisible(!seedOffset.isEmpty()); + ui->line_seedOffset->setText(seedOffset); + + connect(ui->check_toggleSeedType, &QCheckBox::toggled, [this, seed_25_words, seed](bool toggled){ + this->setSeed(toggled ? seed_25_words : seed); + ui->frameRestoreHeight->setVisible(toggled); + }); } - ui->frameSeedOffset->setVisible(!seedOffset.isEmpty()); - ui->line_seedOffset->setText(seedOffset); - - connect(ui->check_toggleSeedType, &QCheckBox::toggled, [this, seed_25_words, seed](bool toggled){ - this->setSeed(toggled ? seed_25_words : seed); - ui->frameRestoreHeight->setVisible(toggled); - }); - ui->label_restoreHeightHelp->setHelpText("", "Should you restore your wallet in the future, " "specifying this block number will recover your wallet quicker.", "restore_height"); @@ -59,10 +66,23 @@ void SeedDialog::setSeed(const QString &seed) { "

" "WARNING:" "
    " - "
  • Never disclose your seed.
  • " + "
  • Never disclose your seed
  • " "
  • Never type it on a website
  • " "
  • Do not store it electronically
  • " "
").arg(words)); } +void SeedDialog::setMultisigSeed(const QString &seed) { + ui->seed->setPlainText(seed); + + ui->label_warning->setText("

This seed will allow you to recover your wallet in case " + "of computer failure." + "

" + "WARNING:" + "
    " + "
  • Never disclose your seed
  • " + "
  • Never type it on a website
  • " + "
"); +} + SeedDialog::~SeedDialog() = default; \ No newline at end of file diff --git a/src/dialog/SeedDialog.h b/src/dialog/SeedDialog.h index ccdbb8b..a8dafd7 100644 --- a/src/dialog/SeedDialog.h +++ b/src/dialog/SeedDialog.h @@ -23,6 +23,7 @@ public: private: void setSeed(const QString &seed); + void setMultisigSeed(const QString &seed); QScopedPointer ui; Wallet *m_wallet; diff --git a/src/dialog/TxConfAdvDialog.cpp b/src/dialog/TxConfAdvDialog.cpp index 4b8a13a..390f40f 100644 --- a/src/dialog/TxConfAdvDialog.cpp +++ b/src/dialog/TxConfAdvDialog.cpp @@ -103,6 +103,38 @@ void TxConfAdvDialog::setUnsignedTransaction(UnsignedTransaction *utx) { this->setupConstructionData(ci); } +void TxConfAdvDialog::setMultisigTransaction(PendingTransaction *tx) { + m_multisig = true; + m_tx = tx; + m_tx->refresh(); + + ui->btn_exportSigned->hide(); + ui->btn_exportTxKey->hide(); + ui->btn_sign->show(); + ui->btn_send->show(); + ui->btn_send->setEnabled(false); + + if (m_tx->haveWeSigned()) { + ui->btn_sign->setEnabled(false); + } + + if (m_tx->enoughMultisigSignatures()) { + ui->btn_send->setEnabled(true); + } + +// ui->btn_send->setText(m_tx->signaturesNeeded() == 1 ? "S"); +// +// if (m_tx->signaturesNeeded() == 1) { +// +// } + + PendingTransactionInfo *ptx = m_tx->transaction(0); + + ui->txid->setText("n/a"); + this->setAmounts(tx->amount(), tx->fee()); + this->setupConstructionData(ptx); +} + void TxConfAdvDialog::setAmounts(quint64 amount, quint64 fee) { QString preferredCur = conf()->get(Config::preferredFiatCurrency).toString(); @@ -180,6 +212,29 @@ void TxConfAdvDialog::setupConstructionData(ConstructionInfo *ci) { } void TxConfAdvDialog::signTransaction() { + if (m_multisig) { + + m_tx->signMultisigTx(); + if (m_tx->status() != PendingTransaction::Status_Ok) { + Utils::showError(this, "Unable to sign multisig transaction", m_tx->errorString()); + +// auto button = QMessageBox::question(this, "Export multisig info?", "Do you want to export multisig info now?"); +// if (result == QMessageBox::Yes) { +// m_wallet->mmsStore()->ex +// } + + return; + } + + if (m_tx->enoughMultisigSignatures()) { + ui->btn_send->setText("Broadcast"); + } + + ui->btn_sign->setEnabled(false); + ui->btn_send->setEnabled(true); + return; + } + this->accept(); } diff --git a/src/dialog/TxConfAdvDialog.h b/src/dialog/TxConfAdvDialog.h index e0faba5..3d46274 100644 --- a/src/dialog/TxConfAdvDialog.h +++ b/src/dialog/TxConfAdvDialog.h @@ -28,6 +28,7 @@ public: void setTransaction(PendingTransaction *tx, bool isSigned = true); // #TODO: have libwallet return a UnsignedTransaction, this is just dumb void setUnsignedTransaction(UnsignedTransaction *utx); + void setMultisigTransaction(PendingTransaction *tx); private: void setupConstructionData(ConstructionInfo *ci); @@ -51,6 +52,7 @@ private: QMenu *m_exportTxKeyMenu; QString m_txid; bool m_offline; + bool m_multisig = false; }; #endif //FEATHER_TXCONFADVDIALOG_H diff --git a/src/dialog/TxConfDialog.cpp b/src/dialog/TxConfDialog.cpp index cb63d92..8c42425 100644 --- a/src/dialog/TxConfDialog.cpp +++ b/src/dialog/TxConfDialog.cpp @@ -76,13 +76,27 @@ TxConfDialog::TxConfDialog(Wallet *wallet, PendingTransaction *tx, const QString ui->label_fee->setToolTip("Unrealistic fee. You may be connected to a malicious node."); } - ui->buttonBox->button(QDialogButtonBox::Ok)->setText("Send"); + bool readyToSend = tx->enoughMultisigSignatures(); + + if (!readyToSend) { + ui->combo_sendTo->addItem("All cosigners"); + ui->combo_sendTo->addItems(m_wallet->getMultisigSigners()); + } else { + ui->label_sendTo->hide(); + ui->combo_sendTo->hide(); + } + + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(!readyToSend ? "Sign" : "Send"); connect(ui->btn_Advanced, &QPushButton::clicked, this, &TxConfDialog::setShowAdvanced); this->adjustSize(); } +quint32 TxConfDialog::getMultisigSignerIndex() { + return ui->combo_sendTo->currentIndex(); +} + void TxConfDialog::setShowAdvanced() { this->showAdvanced = true; QDialog::reject(); diff --git a/src/dialog/TxConfDialog.h b/src/dialog/TxConfDialog.h index e1be67e..27dedf1 100644 --- a/src/dialog/TxConfDialog.h +++ b/src/dialog/TxConfDialog.h @@ -23,6 +23,8 @@ public: explicit TxConfDialog(Wallet *wallet, PendingTransaction *tx, const QString &address, const QString &description, QWidget *parent = nullptr); ~TxConfDialog() override; + quint32 getMultisigSignerIndex(); + bool showAdvanced = false; private: diff --git a/src/dialog/TxConfDialog.ui b/src/dialog/TxConfDialog.ui index 5ead8f4..955ff7f 100644 --- a/src/dialog/TxConfDialog.ui +++ b/src/dialog/TxConfDialog.ui @@ -119,6 +119,16 @@
+ + + + Send to: + + + + + + diff --git a/src/libwalletqt/Coins.cpp b/src/libwalletqt/Coins.cpp index 7ab8a45..ed424f1 100644 --- a/src/libwalletqt/Coins.cpp +++ b/src/libwalletqt/Coins.cpp @@ -76,6 +76,23 @@ void Coins::refresh() ci->m_coinbase = td.m_tx.vin.size() == 1 && td.m_tx.vin[0].type() == typeid(cryptonote::txin_gen); ci->m_description = m_wallet->getCacheAttribute(QString("coin.description:%1").arg(ci->m_pubKey)); ci->m_change = m_wallet2->is_change(td); + ci->m_keyImagePartial = td.m_key_image_partial; + + bool haveMultisigK = true; + if (td.m_multisig_k.empty()) { + haveMultisigK = false; + } + for (const auto &k : td.m_multisig_k) { + if (k == rct::zero()) { + haveMultisigK = false; + break; + } + } + ci->m_haveMultisigK = haveMultisigK; + + for (const auto& info : td.m_multisig_info) { + ci->m_multisigInfo.append(QString::fromStdString(m_wallet2->get_signer_label(info.m_signer))); + } m_rows.push_back(ci); } diff --git a/src/libwalletqt/Coins.h b/src/libwalletqt/Coins.h index 4c6b252..a6f6f12 100644 --- a/src/libwalletqt/Coins.h +++ b/src/libwalletqt/Coins.h @@ -30,7 +30,6 @@ Q_OBJECT public: bool coin(int index, std::function callback); CoinsInfo * coin(int index); - void refresh(); void refreshUnlocked(); void freeze(QString &publicKey); void thaw(QString &publicKey); @@ -41,6 +40,9 @@ public: quint64 count() const; void clearRows(); +public slots: + void refresh(); + signals: void refreshStarted() const; void refreshFinished() const; diff --git a/src/libwalletqt/MultisigMessageStore.cpp b/src/libwalletqt/MultisigMessageStore.cpp new file mode 100644 index 0000000..17385ee --- /dev/null +++ b/src/libwalletqt/MultisigMessageStore.cpp @@ -0,0 +1,1025 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "MultisigMessageStore.h" + +#include "Coins.h" +#include "rows/CoinsInfo.h" +#include +#include "rows/MultisigMessage.h" +#include "rows/TxProposal.h" +#include "libwalletqt/Input.h" +#include "utils/Utils.h" +#include "multisig/multisig_account.h" + +MultisigMessageStore::MultisigMessageStore(Wallet *wallet, tools::wallet2 *wallet2, QObject *parent) + : QObject(parent) + , m_wallet(wallet) + , m_wallet2(wallet2) +{ + +} + +bool MultisigMessageStore::message(int index, std::function callback) +{ + QReadLocker locker(&m_lock); + + if (index < 0 || index >= m_rows.size()) { + qCritical("%s: no transaction info for index %d", __FUNCTION__, index); + qCritical("%s: there's %lld transactions in backend", __FUNCTION__, m_rows.count()); + return false; + } + + callback(*m_rows.value(index)); + return true; +} + +bool MultisigMessageStore::txProposal(int index, std::function callback) +{ + QReadLocker locker(&m_lock); + + if (index < 0 || index >= m_txProposals.size()) { + qCritical("%s: no transaction info for index %d", __FUNCTION__, index); + qCritical("%s: there's %lld transactions in backend", __FUNCTION__, m_txProposals.count()); + return false; + } + + callback(*m_txProposals.value(index)); + return true; +} + + +MultisigMessage* MultisigMessageStore::message(int index) +{ + return m_rows.value(index); +} + +bool MultisigMessageStore::signTx(quint32 id) { + mms::message_store& ms = m_wallet2->get_message_store(); + mms::message m; + bool valid_id = ms.get_message_by_id(id, m); + if (!valid_id) { + return false; + } + return signMultisigTx(m.content); +} + +bool MultisigMessageStore::deleteMessage(quint32 id) { + mms::message_store& ms = m_wallet2->get_message_store(); + mms::message m; + bool valid_id = ms.get_message_by_id(id, m); + if (!valid_id) { + return false; + } + ms.delete_message(m.id); + this->refresh(false); + return true; +} + +void MultisigMessageStore::deleteAllMessages() { + mms::message_store& ms = m_wallet2->get_message_store(); + ms.delete_all_messages(); + this->refresh(false); +} + +std::string MultisigMessageStore::exportMessage(quint32 id) { + mms::message_store& ms = m_wallet2->get_message_store(); + mms::message m; + bool valid_id = ms.get_message_by_id(id, m); + if (!valid_id) { + return ""; + } + + return m.content; +} + +void MultisigMessageStore::receiveMessages() +{ + // Beware! + // This function will always be called from the refresh thread + + qDebug() << "receiveMessages"; + + std::vector new_messages; + mms::message_store& ms = m_wallet2->get_message_store(); + + ms.register_user(); + + if (!ms.signer_keys_complete()) { + uint32_t users; + ms.get_channel_users(m_wallet2->get_multisig_wallet_state(), users); + statusChanged(QString("Waiting for cosigners (%1/%2)").arg(QString::number(users), QString::number(ms.get_num_authorized_signers())), false); + return; + } + + bool avail; + try { + avail = ms.check_for_messages(m_wallet2->get_multisig_wallet_state(), new_messages); + } + catch (std::exception &e) { + // TODO: set status + return; + } + + for (const auto &msg : new_messages) { + if (msg.type == mms::message_type::multisig_sync_data) { + this->processSyncData(); + break; + } + } + + // Send any messages that couldn't be sent due to connection issues + this->sendReadyMessages(); + + if (avail) { + this->refresh(); + } +} + +void MultisigMessageStore::refresh(bool next) +{ + qDebug() << "MultisigMessageStore::refresh"; + + QReadLocker locker(&m_lock); + + emit refreshStarted(); + + bool haveWaiting = false; + + { + clearRows(); + mms::message_store& ms = m_wallet2->get_message_store(); + auto messages = ms.get_all_messages(); + + for (const auto &message : messages) + { + auto msg = new MultisigMessage(this); + msg->id = message.id; + msg->type = QString::fromStdString(mms::message_store::message_type_to_string(message.type)); + msg->direction = QString::fromStdString(mms::message_store::message_direction_to_string(message.direction)); + msg->content = message.content; + msg->created = QDateTime::fromSecsSinceEpoch(message.created); + msg->modified = QDateTime::fromSecsSinceEpoch(message.modified); + msg->sent = QDateTime::fromSecsSinceEpoch(message.sent); + msg->signer_index = message.signer_index; + msg->signer = QString::fromStdString(ms.signer_to_string(ms.get_signer(message.signer_index), 100)); + msg->state = QString::fromStdString(mms::message_store::message_state_to_string(message.state)); + msg->hash = QString::fromStdString(epee::string_tools::pod_to_hex(message.hash)); + msg->wallet_height = message.wallet_height; + msg->round = message.round; + msg->signature_count = message.signature_count; + msg->transport_id = QString::fromStdString(message.transport_id); + + if (message.state == mms::message_state::waiting) { + haveWaiting = true; + } + + if (message.type == mms::message_type::partially_signed_tx && message.direction == mms::message_direction::in) { + PendingTransaction * tx = m_wallet->restoreMultisigTransaction(message.content); + + QString prefixHash = tx->prefixHashes()[0]; + + TxProposal *txProposal; +// if (m_txProposalsIndex.contains(prefixHash)) { +// txProposal = m_txProposals.value(m_txProposalsIndex[prefixHash]); +// } else { +// txProposal = new TxProposal(this); +// } + + txProposal = new TxProposal(this); + txProposal->txCount = tx->txCount(); + txProposal->txId = tx->txid()[0]; // TODO; + txProposal->prefixHash = tx->prefixHashes()[0]; + txProposal->messageId = message.id; + txProposal->timestamp = QDateTime::fromSecsSinceEpoch(message.created); + + txProposal->balanceDelta = tx->amount(); + txProposal->numSignatures = tx->signersKeys().size(); + +// txProposal->from = + + // This doesn't work, we don't know the final txid yet +// if (m_wallet->haveTransaction(txProposal->txId)) { +// txProposal->status = "Completed"; +// } + + txProposal->status = TxProposal::Status::Pending; + + // Decide if we can sign this transaction + + // Are any of the spent outputs frozen? + // Do we have multisig_k for all LR values? + + + tx->refresh(); + auto txx = tx->transaction(0); + + bool completed = false; + bool cant_sign = false; + bool any_frozen = false; + bool double_spend = false; + + for (const auto& input : txx->inputs()) { + txProposal->spendsOutputs.append(input->pubKey().mid(0, 8)); + + crypto::public_key pk; + if (!epee::string_tools::hex_to_pod(input->pubKey().toStdString(), pk)) + { + continue; + } + + auto idx = m_wallet2->get_transfer_details(pk); // TODO: inefficient + auto td = m_wallet2->get_transfer_details(idx); + +// if (td.m_multisig_k.size() == 0) { +// cant_sign = true; +// } +// +// for (const auto &k : td.m_multisig_k) { // weak check, we also need to check LR +// if (k == rct::zero()) { +// cant_sign = true; +// break; +// } +// } + + if (td.m_spent) { + double_spend = true; + } + + if (td.m_frozen) { + any_frozen = true; + } + } + + + crypto::hash prefix_hash; + if (!epee::string_tools::hex_to_pod(txProposal->prefixHash.toStdString(), prefix_hash)) + { + continue; + } + + completed = m_wallet2->have_tx_prefix(prefix_hash); + + if (tx->haveWeSigned()) { + txProposal->status = TxProposal::Status::Signed; + } + else if (!tx->canSign()) { + txProposal->status = TxProposal::Status::Cant_Sign; + } + else if (any_frozen) { + txProposal->status = TxProposal::Status::Frozen; + } + else if (double_spend) { + txProposal->status = TxProposal::Status::Double_Spend; + } + + if (completed) { + txProposal->status = TxProposal::Status::Completed; + } + + m_txProposals.push_back(txProposal); + m_txProposalsIndex[prefixHash] = m_txProposals.length() - 1; + } + + if (message.type == mms::message_type::partially_signed_tx && message.direction == mms::message_direction::out) { + + } + + m_rows.push_back(msg); + } + } + + if (haveWaiting && next) { + this->next(); + } + + emit refreshFinished(); +} + +bool MultisigMessageStore::processSyncData() { + qDebug() << tr("import_multisig_info"); + QWriteLocker locker(&lock); + + mms::message_store& ms = m_wallet2->get_message_store(); + + std::vector messages; + ms.process_sync_data(messages); + + std::vector infos; + for (size_t i = 0; i < messages.size(); ++i) + { + if (messages[i] == 0) { + continue; + } + + mms::message m = ms.get_message_by_id(messages[i]); + infos.push_back(m.content); + } + + bool success = importMultisig(infos); + + this->refresh(); + + return success; +} + +bool MultisigMessageStore::sendPendingTransaction(quint32 id, quint32 cosigner) { + qDebug() << tr("sendPendingTransaction"); + + mms::message_store& ms = m_wallet2->get_message_store(); + mms::message m = ms.get_message_by_id(id); + + if (cosigner == 0) { + // Send to all cosigners except me + for (int i = 1; i < m_wallet2->get_multisig_signers(); i++) { + ms.add_message(m_wallet2->get_multisig_wallet_state(), i, m.type, mms::message_direction::out, m.content); + } + } else { + ms.add_message(m_wallet2->get_multisig_wallet_state(), cosigner, m.type, mms::message_direction::out, m.content); + } + + sendReadyMessages(); + this->refresh(); + return true; +} + +void MultisigMessageStore::next(bool forceSync, bool calledFromRefresh) +{ + qDebug() << "MultisigMessageStore::next"; + + mms::message_store& ms = m_wallet2->get_message_store(); + + bool avail = false; + std::vector data_list; // List of processable message ids + uint32_t choice = 0; + { + std::string wait_reason; + { + avail = ms.get_processable_messages(m_wallet2->get_multisig_wallet_state(), forceSync, data_list, wait_reason); + } + if (!wait_reason.empty()) + { + QString waitReason = QString::fromStdString(wait_reason); + if (!waitReason.contains("Wallet can't")) { + emit statusChanged(QString::fromStdString(wait_reason), false); + } + + qDebug() << "No next step: " << wait_reason; + } + } + + if (avail) + { + qDebug() << "Processing available messages"; + + mms::processing_data data = data_list[choice]; + bool command_successful = false; + switch(data.processing) + { + case mms::message_processing::add_auto_config_data: { + qDebug() << "Add signer config data"; +// ms.add_auto_config_data_messages(m_wallet2->get_multisig_wallet_state()); + command_successful = true; + break; + } + + case mms::message_processing::process_auto_config_data: + { + qDebug() << tr("Process auto config data"); + size_t num_auto_config_data = data.message_ids.size(); + +// emit statusChanged(QString("Waiting for signer info (%1/%2)").arg(QString::number(data.message_ids.size()+1), QString::number(ms.get_num_authorized_signers())), false); +// if (num_auto_config_data < (ms.get_num_authorized_signers()-1)) { +// break; +// } + + for (size_t i = 0; i < num_auto_config_data; ++i) { + ms.process_auto_config_data_message(data.message_ids[i]); + } + + ms.stop_auto_config(); + command_successful = prepareMultisig(); + emit signersUpdated(); + break; + } + + case mms::message_processing::prepare_multisig: + qDebug() << tr("prepare_multisig"); + command_successful = prepareMultisig(); + break; + + case mms::message_processing::make_multisig: + { + qDebug() << tr("make_multisig"); + size_t number_of_key_sets = data.message_ids.size(); + std::vector sig_args(number_of_key_sets); + for (size_t i = 0; i < number_of_key_sets; ++i) + { + mms::message m = ms.get_message_by_id(data.message_ids[i]); + sig_args[i] = m.content; + } + command_successful = makeMultisig(ms.get_num_required_signers(), sig_args); + break; + } + + case mms::message_processing::exchange_multisig_keys: + { + qDebug() << tr("exchange_multisig_keys"); + size_t number_of_key_sets = data.message_ids.size(); + // Other than "make_multisig" only the key sets as parameters, no num_required_signers + std::vector sig_args(number_of_key_sets); + for (size_t i = 0; i < number_of_key_sets; ++i) + { + mms::message m = ms.get_message_by_id(data.message_ids[i]); + sig_args[i] = m.content; + } + // todo: update mms to enable 'key exchange force updating' + command_successful = exchangeMultisig(sig_args); + break; + } + + case mms::message_processing::create_sync_data: + { +// qDebug() << tr("export_multisig_info"); +// command_successful = exportMultisig(); + break; + } + + case mms::message_processing::process_sync_data: + { +// qDebug() << tr("import_multisig_info"); +// std::vector infos; +// for (size_t i = 0; i < data.message_ids.size(); ++i) +// { +// mms::message m = ms.get_message_by_id(data.message_ids[i]); +// infos.push_back(m.content); +// } +// command_successful = importMultisig(infos); + break; + } + + case mms::message_processing::sign_tx: + { + qDebug() << tr("sign_multisig"); +// mms::message m = ms.get_message_by_id(data.message_ids[0]); +// command_successful = signMultisigTx(m.content); + break; + } + + case mms::message_processing::submit_tx: + { + qDebug() << tr("submit_multisig"); +// mms::message m = ms.get_message_by_id(data.message_ids[0]); +// command_successful = submitMultisigTx(m.content); + break; + } + + case mms::message_processing::process_signer_config: + { + qDebug() << tr("Process signer config"); +// LOCK_IDLE_SCOPE(); +// mms::message m = ms.get_message_by_id(data.message_ids[0]); +// mms::authorized_signer me = ms.get_signer(0); +// mms::multisig_wallet_state state = get_multisig_wallet_state(); +// if (!me.auto_config_running) +// { +// // If no auto-config is running, the config sent may be unsolicited or problematic +// // so show what arrived and ask for confirmation before taking it in +// std::vector signers; +// ms.unpack_signer_config(state, m.content, signers); +// list_signers(signers); +// if (!user_confirms(tr("Replace current signer config with the one displayed above?"))) +// { +// break; +// } +// if (!user_confirms_auto_config()) +// { +// message_writer() << tr("You can use the \"mms delete\" command to delete any unwanted message"); +// break; +// } +// } +// ms.process_signer_config(state, m.content); +// ms.stop_auto_config(); +// list_signers(ms.get_all_signers()); +// command_successful = true; + break; + } + + default: + qDebug() << tr("Nothing ready to process"); + break; + } + + if (command_successful) + { + { +// LOCK_IDLE_SCOPE(); + ms.set_messages_processed(data); + sendReadyMessages(); + this->refresh(); + } + } + } +} + +void MultisigMessageStore::sendReadyMessages() { + qDebug() << "sendReadyMessages"; + mms::message_store& ms = m_wallet2->get_message_store(); + std::vector ready_messages; + const std::vector &messages = ms.get_all_messages(); + for (size_t i = 0; i < messages.size(); ++i) + { + const mms::message &m = messages[i]; + if (m.state == mms::message_state::ready_to_send) + { + ready_messages.push_back(m); + } + } + + mms::multisig_wallet_state state = m_wallet2->get_multisig_wallet_state(); + for (size_t i = 0; i < ready_messages.size(); ++i) + { + try { + ms.send_message(state, ready_messages[i].id); + } + catch (std::exception &e) { + emit connectionError(); + qWarning() << "Unable to send MMS message"; + return; + } + + ms.set_message_processed_or_sent(ready_messages[i].id); + } + qDebug() << "Queued for sending"; +} + +bool MultisigMessageStore::prepareMultisig() { + std::string multisig_info = m_wallet2->get_multisig_first_kex_msg(); + mms::message_store& ms = m_wallet2->get_message_store(); + std::vector message_ids; + ms.process_wallet_created_data(m_wallet2->get_multisig_wallet_state(), mms::message_type::key_set, multisig_info, message_ids); + + uint32_t rounds = multisig::multisig_setup_rounds_required(ms.get_num_authorized_signers(), ms.get_num_required_signers()); + emit statusChanged(QString("Exchanging keys (1/%1)").arg(QString::number(rounds)), false); + return true; +} + +QString MultisigMessageStore::errorString() { + return m_errorString; +} + +void MultisigMessageStore::setErrorString(const QString &errorString) { + m_errorString = errorString; +} + +void MultisigMessageStore::clearStatus() { + m_errorString = ""; +} + +void MultisigMessageStore::setServiceDetails(const QString &serviceUrl, const QString &serviceLogin) { + // TODO: make sure we respect "only allow connections to .onion services" + + mms::message_store& ms = m_wallet2->get_message_store(); + ms.set_service_details(serviceUrl.toStdString(), serviceLogin.toStdString()); +} + +bool MultisigMessageStore::registerChannel(QString &channel, quint32 user_limit) { + mms::message_store& ms = m_wallet2->get_message_store(); + bool success; + + clearStatus(); + + try { + std::string channel_std; + success = ms.register_channel(channel_std, user_limit); + channel = QString::fromStdString(channel_std); + } + catch (const std::exception &e) { + this->setErrorString(e.what()); + return false; + } + + return success; +} + +bool MultisigMessageStore::makeMultisig(quint32 threshold, const std::vector &kexMessages) { + try + { + std::string multisig_extra_info = m_wallet2->make_multisig("", kexMessages, threshold); + bool ready; + m_wallet2->multisig(&ready); + if (!ready) + { + std::vector message_ids; + m_wallet2->get_message_store().process_wallet_created_data(m_wallet2->get_multisig_wallet_state(), mms::message_type::additional_key_set, multisig_extra_info, message_ids); + emit statusChanged(QString("Exchanging keys (2/%2)").arg(QString::number(m_wallet2->get_multisig_setup_rounds_required())), false); + return true; + } + } + catch (const std::exception &e) + { + qDebug() << tr("Error creating multisig: ") << e.what(); + return false; + } + + uint32_t total; + if (!m_wallet2->multisig(NULL, &threshold, &total)) + { + qDebug() << tr("Error creating multisig: new wallet is not multisig"); + return false; + } + qDebug() << std::to_string(threshold) << "/" << total << tr(" multisig address: ") + << m_wallet2->get_account().get_public_address_str(m_wallet2->nettype()); + + + return true; +} + +bool MultisigMessageStore::exchangeMultisig(const std::vector &kexMessages) { + try + { + std::string multisig_extra_info = m_wallet2->exchange_multisig_keys("", kexMessages, false); + bool ready; + m_wallet2->multisig(&ready); + if (!ready) + { + auto state = m_wallet2->get_multisig_wallet_state(); + emit statusChanged(QString("Exchanging keys (%1/%2)").arg(QString::number(state.multisig_rounds_passed + 1), QString::number(m_wallet2->get_multisig_setup_rounds_required())), false); + + std::vector message_ids; + m_wallet2->get_message_store().process_wallet_created_data(m_wallet2->get_multisig_wallet_state(), mms::message_type::additional_key_set, multisig_extra_info, message_ids); + + return true; + } else { + uint32_t threshold, total; + m_wallet2->multisig(NULL, &threshold, &total); + emit statusChanged("Multisig wallet has been created", true); + emit multisigWalletCreated(QString::fromStdString(m_wallet2->get_account().get_public_address_str(m_wallet2->nettype()))); + qDebug() << tr("Multisig wallet has been successfully created. Current wallet type: ") << threshold << "/" << total; + qDebug() << tr("Multisig address: ") << m_wallet2->get_account().get_public_address_str(m_wallet2->nettype()); + } + } + catch (const std::exception &e) + { + qDebug() << tr("Failed to perform multisig keys exchange: ") << e.what(); + return false; + } + + return true; +} + +bool MultisigMessageStore::exportMultisig() { + bool ready; + if (!m_wallet2->multisig(&ready)) { + qWarning() << "This wallet is not multisig"; + return false; + } + if (!ready) { + qWarning() << "This multisig wallet is not yet finalized"; + return false; + } + + qDebug() << "exportMultisig: current thread: " << QThread::currentThread(); + + QWriteLocker locker(&lock); + + // Calling this function will reset any nonces recorded by the previous call to this function. Doing so will + // invalidate any in-progress signing attempts that rely on the previous output of this function. + + // This function should be called if: + // - There are any transfer details with m_key_image_known == false + + // std::vector info; (one multisig_info per owned output) + // + // multisig_info: + // crypto::public_key m_signer = get_multisig_signer_public_key(); (our public spend key) + // std::vector m_LR; (nlr * 2 times) + // std::vector m_partial_key_images; (one for each m_multisig_keys) + // + // LR: + // rct::key m_L; = k * G (nonce public key) + // rct::key m_R; = k * H(output public key) (nonce key image) + + + // The following state is affected: + // + // transfer_details: (m_transfers) + // std::vector m_multisig_k; (nlr * 2 nonces (k)) + + try + { + cryptonote::blobdata ciphertext = m_wallet2->export_multisig(); + std::vector message_ids; + m_wallet2->get_message_store().process_wallet_created_data(m_wallet2->get_multisig_wallet_state(), mms::message_type::multisig_sync_data, ciphertext, message_ids); + } + catch (const std::exception &e) + { + qWarning() << tr("Error exporting multisig info: ") << e.what(); + return false; + } + + sendReadyMessages(); + this->refresh(); + + emit multisigInfoExported(); + + qDebug() << "Multisig info exported to MMS"; + return true; +} + +bool MultisigMessageStore::importMultisig(const std::vector info) { + bool ready; + uint32_t threshold, total; + if (!m_wallet2->multisig(&ready, &threshold, &total)) { + qWarning() << "This wallet is not multisig"; + return false; + } + if (!ready) { + qWarning() << "This multisig wallet is not yet finalized"; + return false; + } + + qDebug() << "importMultisig: current thread: " << QThread::currentThread(); + + // The following state is affected: + // + // transfer_details: (m_transfers) + // std::vector m_multisig_info; (one multisig_info per other participant) + // crypto::key_image m_key_image; (calculated using m_partial_key_images in m_multisig_info) + // bool m_key_image_known = true; + // bool m_key_image_request = false; + // bool m_key_image_partial = false; + + try + { + // TODO: need to be synced here; + size_t n_outputs = m_wallet2->import_multisig(info); + } + catch (const std::exception &e) + { + qWarning() << tr("Failed to import multisig info: ") << e.what(); + return true; + } + + try + { + m_wallet2->rescan_spent(); + } + catch (const std::exception &e) + { + qWarning() << tr("Failed to update spent status after importing multisig info: ") << e.what(); + return false; + } + + emit multisigInfoImported(); + + return true; +} + +bool MultisigMessageStore::signMultisigTx(const cryptonote::blobdata &data) { + PendingTransaction * tx = m_wallet->restoreMultisigTransaction(data); + emit askToSign(tx); + +// std::vector txids; +// uint32_t signers = 0; +// try +// { +// tools::wallet2::multisig_tx_set exported_txs; +// std::string ciphertext; +// bool r = m_wallet2->load_multisig_tx(data, exported_txs, [&](const tools::wallet2::multisig_tx_set &tx){ +// signers = tx.m_signers.size(); +// return true; +// }); +// if (r) +// { +// r = m_wallet2->sign_multisig_tx(exported_txs, txids); +// } +// if (r) +// { +// ciphertext = m_wallet2->save_multisig_tx(exported_txs); +// if (ciphertext.empty()) +// { +// r = false; +// } +// } +// if (r) +// { +// mms::message_type message_type = mms::message_type::fully_signed_tx; +// if (txids.empty()) +// { +// message_type = mms::message_type::partially_signed_tx; +// } +// m_wallet2->get_message_store().process_wallet_created_data(m_wallet2->get_multisig_wallet_state(), message_type, ciphertext); +//// filename = "MMS"; // for the messages below +// } +// else +// { +// qWarning() << tr("Failed to sign multisig transaction"); +// return false; +// } +// }ye +// catch (const tools::error::multisig_export_needed& e) +// { +// qWarning() << tr("Multisig error: ") << e.what(); +// return false; +// } +// catch (const std::exception &e) +// { +// qWarning() << tr("Failed to sign multisig transaction: ") << e.what(); +// return false; +// } + +// if (txids.empty()) +// { +// uint32_t threshold; +// m_wallet2->multisig(NULL, &threshold); +// uint32_t signers_needed = threshold - signers - 1; +// success_msg_writer(true) << tr("Transaction successfully signed to file ") << filename << ", " +// << signers_needed << " more signer(s) needed"; +// return true; +// } +// else +// { +// std::string txids_as_text; +// for (const auto &txid: txids) +// { +// if (!txids_as_text.empty()) +// txids_as_text += (", "); +// txids_as_text += epee::string_tools::pod_to_hex(txid); +// } +// success_msg_writer(true) << tr("Transaction successfully signed to file ") << filename << ", txid " << txids_as_text; +// success_msg_writer(true) << tr("It may be relayed to the network with submit_multisig"); +// } + return true; +} + +bool MultisigMessageStore::submitMultisigTx(const cryptonote::blobdata &data) { + bool ready; + uint32_t threshold; + if (!m_wallet2->multisig(&ready, &threshold)) + { + qDebug() << tr("This is not a multisig wallet"); + return false; + } + + try + { + tools::wallet2::multisig_tx_set txs; + + bool r = m_wallet2->load_multisig_tx(data, txs, [&](const tools::wallet2::multisig_tx_set &tx){ return true; }); + if (!r) + { + qDebug() << tr("Failed to load multisig transaction from MMS"); + return false; + } + + + if (txs.m_signers.size() < threshold) + { + qDebug() << QString("Multisig transaction signed by only %1 signers, needs %2 more signatures").arg(QString::number(txs.m_signers.size()), QString::number(threshold - txs.m_signers.size())); + return false; + } + + // actually commit the transactions + for (auto &ptx: txs.m_ptx) + { + m_wallet2->commit_tx(ptx); + qDebug() << tr("Transaction successfully submitted, transaction "); + } + } + catch (const std::exception &e) + { + qWarning() << "Something terrible happened"; +// handle_transfer_exception(std::current_exception(), m_wallet2->is_trusted_daemon()); + } + catch (...) + { + LOG_ERROR("unknown error"); + qDebug() << tr("unknown error"); + return false; + } + + return true; +} + +bool MultisigMessageStore::havePartiallySignedTxWaiting() { + for (const auto & m : m_rows) { + if (m->type == "partially signed tx" && m->state == "waiting") { + return true; + } + } + return false; +} + +QString MultisigMessageStore::createSetupKey(quint32 threshold, quint32 signers, const QString &service, const QString &channel, SetupMode mode) { + mms::message_store& ms = m_wallet2->get_message_store(); + + QString setupKey; + try { + std::string key = ms.create_setup_key(threshold, signers, service.toStdString(), channel.toStdString(), static_cast(mode)); + setupKey = QString::fromStdString(key); + } + catch (const std::exception &e) { + return ""; + } + + return setupKey; +} + +bool MultisigMessageStore::checkSetupKey(const QString &setupKeyStr, SetupKey &setupKey) { + mms::message_store& ms = m_wallet2->get_message_store(); + + std::string adjusted_token; + mms::setup_key key; + bool tokenValid = ms.check_auto_config_token(setupKeyStr.toStdString(), key); + + if (tokenValid) { + setupKey.threshold = key.threshold; + setupKey.participants = key.participants; + setupKey.service = QString::fromStdString(key.service_url); + setupKey.mode = static_cast(key.mode); + } + + return tokenValid; +} + +void MultisigMessageStore::init(const QString &setupKey, const QString &ownLabel) { + mms::message_store& ms = m_wallet2->get_message_store(); + ms.init_from_setup_key(m_wallet2->get_multisig_wallet_state(), setupKey.toStdString(), ownLabel.toStdString()); +} + +void MultisigMessageStore::setSigner(quint32 index, const QString& label, const QString& address) { + mms::message_store& ms = m_wallet2->get_message_store(); + + // TODO: do sanity checks + + cryptonote::address_parse_info info; + if (!get_account_address_from_str(info, static_cast(m_wallet2->nettype()), address.toStdString())) { + return; + } + + ms.set_signer(m_wallet2->get_multisig_wallet_state(), index, label.toStdString(), {}); +} + +QVector MultisigMessageStore::getSignerInfo() { + QWriteLocker locker(&lock); + mms::message_store& ms = m_wallet2->get_message_store(); + + QVector signerInfo; + + for (size_t i = 0; i < ms.get_num_authorized_signers(); i++) { + mms::authorized_signer signer = ms.get_signer(i); + + if (!signer.public_key_known) { + continue; + } + + SignerInfo info; + info.label = QString::fromStdString(signer.label); + info.publicKey = QString::fromStdString(epee::string_tools::pod_to_hex(signer.public_key)); + signerInfo.push_back(info); + } + + return signerInfo; +} + +QString MultisigMessageStore::getRecoveryInfo() { + QWriteLocker locker(&lock); + mms::message_store& ms = m_wallet2->get_message_store(); + + std::string info = ms.get_recovery_info(m_wallet2->get_multisig_wallet_state(), m_wallet2->get_refresh_from_block_height()); + return QString::fromStdString(info); +} + +quint64 MultisigMessageStore::count() const +{ + QReadLocker locker(&m_lock); + + return m_rows.length(); +} + +quint64 MultisigMessageStore::txProposalCount() const { + QReadLocker locker(&m_lock); + + return m_txProposals.size(); +} + + +void MultisigMessageStore::clearRows() { + qDeleteAll(m_rows); + m_rows.clear(); + + qDeleteAll(m_txProposals); + m_txProposals.clear(); +} + +MultisigMessageStore::SignerInfo MultisigMessageStore::getSignerInfo(quint32 index) { + SignerInfo info; + mms::message_store& ms = m_wallet2->get_message_store(); + mms::authorized_signer signer = ms.get_signer(index); + + info.label = QString::fromStdString(signer.label); + info.publicKey = QString::fromStdString(epee::string_tools::pod_to_hex(signer.public_key)); + + return info; +} diff --git a/src/libwalletqt/MultisigMessageStore.h b/src/libwalletqt/MultisigMessageStore.h new file mode 100644 index 0000000..bae2477 --- /dev/null +++ b/src/libwalletqt/MultisigMessageStore.h @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef FEATHER_MULTISIGMESSAGESTORE_H +#define FEATHER_MULTISIGMESSAGESTORE_H + +#include + +#include +#include +#include +#include + +#include "Wallet.h" + +#include "wallet/message_store.h" +#include "cryptonote_basic/blobdatatype.h" + +namespace tools { + class wallet2; +} + +class MultisigMessage; +class TxProposal; + +class MultisigMessageStore : public QObject +{ +Q_OBJECT + +public: + struct SignerInfo { + QString label; + QString publicKey; + }; + + enum SetupMode { + AUTOMATIC = 0, + SEMI_AUTOMATIC, + MANUAL + }; + + struct SetupKey { + quint32 threshold; + quint32 participants; + QString service; + SetupMode mode; + }; + + bool txProposal(int index, std::function callback); + quint64 txProposalCount() const; + + + bool message(int index, std::function callback); + + void receiveMessages(); + + MultisigMessage * message(int index); + void refresh(bool next = true); + + void next(bool forceSync = false, bool calledFromRefresh = false); + + bool signTx(quint32 id); + bool deleteMessage(quint32 id); + void deleteAllMessages(); + + std::string exportMessage(quint32 id); + + void sendReadyMessages(); + + QString errorString(); + + void setServiceDetails(const QString &serviceUrl, const QString &serviceLogin); + + bool registerChannel(QString &channel, quint32 user_limit); + + bool prepareMultisig(); + bool makeMultisig(quint32 threshold, const std::vector &kexMessages); + bool exchangeMultisig(const std::vector &kexMessages); + bool exportMultisig(); + bool importMultisig(const std::vector info); + bool signMultisigTx(const cryptonote::blobdata &data); + bool submitMultisigTx(const cryptonote::blobdata &data); + + quint64 count() const; + void clearRows(); + + bool havePartiallySignedTxWaiting(); + + SignerInfo getSignerInfo(quint32 index); + + bool processSyncData(); + bool sendPendingTransaction(quint32 message_id, quint32 cosigner); + + QString createSetupKey(quint32 threshold, quint32 signers, const QString &service, const QString &channel, SetupMode mode); + bool checkSetupKey(const QString &setupKey, SetupKey &key); + + void init(const QString &setupKey, const QString &ownLabel); + void setSigner(quint32 index, const QString& label, const QString& address); + + QVector getSignerInfo(); + + QString getRecoveryInfo(); + +signals: + void refreshStarted() const; + void refreshFinished() const; + void multisigWalletCreated(const QString &address); + void signersUpdated() const; + + void askToSign(PendingTransaction *tx) const; + void statusChanged(const QString &status, bool finished); + + void multisigInfoExported(); + void multisigInfoImported(); + + void connectionError(); + +private: + explicit MultisigMessageStore(Wallet *wallet, tools::wallet2 *wallet2, QObject *parent = nullptr); + + void setErrorString(const QString &errorString); + void clearStatus(); + +private: + friend class Wallet; + + Wallet *m_wallet; + tools::wallet2 *m_wallet2; + QList m_rows; + + QList m_txProposals; + QHash m_txProposalsIndex; + +// QList m_txProposals; + QReadWriteLock lock{QReadWriteLock::RecursionMode::Recursive}; + + quint32 m_sendToIndex = 1; + QString m_status; + QString m_errorString; + + mutable QReadWriteLock m_lock; +}; + +#endif //FEATHER_MULTISIGMESSAGESTORE_H diff --git a/src/libwalletqt/PendingTransaction.cpp b/src/libwalletqt/PendingTransaction.cpp index 68767f5..216246f 100644 --- a/src/libwalletqt/PendingTransaction.cpp +++ b/src/libwalletqt/PendingTransaction.cpp @@ -53,6 +53,14 @@ QStringList PendingTransaction::txid() const return list; } +QStringList PendingTransaction::prefixHashes() const +{ + QStringList prefixHashes; + for (const auto &hash : m_pimpl->prefixHashes()) { + prefixHashes.append(QString::fromStdString(hash)); + } + return prefixHashes; +} quint64 PendingTransaction::txCount() const { @@ -98,6 +106,48 @@ void PendingTransaction::refresh() } } +quint32 PendingTransaction::saveToMMS() { + return m_pimpl->saveToMMS(); +} + +QStringList PendingTransaction::destinationAddresses(int index) { + std::vector dests = m_pimpl->destinations(index); + QStringList destinations; + for (const auto &dest : dests) { + destinations << QString::fromStdString(dest); + } + + return destinations; +} + +void PendingTransaction::signMultisigTx() { + m_pimpl->signMultisigTx(); +} + +quint64 PendingTransaction::signaturesNeeded() { + return m_pimpl->signaturesNeeded(); +} + +bool PendingTransaction::enoughMultisigSignatures() { + return m_pimpl->enoughMultisigSignatures(); +} + +QStringList PendingTransaction::signersKeys() { + QStringList keys; + for (const auto &key : m_pimpl->signersKeys()) { + keys.append(QString::fromStdString(key)); + } + return keys; +} + +bool PendingTransaction::haveWeSigned() const { + return m_pimpl->haveWeSigned(); +} + +bool PendingTransaction::canSign() const { + return m_pimpl->canSign(); +} + PendingTransaction::PendingTransaction(Monero::PendingTransaction *pt, QObject *parent) : QObject(parent) , m_pimpl(pt) diff --git a/src/libwalletqt/PendingTransaction.h b/src/libwalletqt/PendingTransaction.h index 9e71f71..cfe3ba5 100644 --- a/src/libwalletqt/PendingTransaction.h +++ b/src/libwalletqt/PendingTransaction.h @@ -31,12 +31,23 @@ public: quint64 dust() const; quint64 fee() const; QStringList txid() const; + QStringList prefixHashes() const; quint64 txCount() const; QList subaddrIndices() const; std::string unsignedTxToBin() const; QString unsignedTxToBase64() const; QString signedTxToHex(int index) const; void refresh(); + quint32 saveToMMS(); + QStringList destinationAddresses(int index); + void signMultisigTx(); + quint64 signaturesNeeded(); + bool enoughMultisigSignatures(); + + QStringList signersKeys(); + + bool haveWeSigned() const; + bool canSign() const; PendingTransactionInfo * transaction(int index) const; diff --git a/src/libwalletqt/TransactionHistory.cpp b/src/libwalletqt/TransactionHistory.cpp index 37ee863..cf6a87d 100644 --- a/src/libwalletqt/TransactionHistory.cpp +++ b/src/libwalletqt/TransactionHistory.cpp @@ -185,6 +185,10 @@ void TransactionHistory::refresh() t->m_rings.append(ring); } + qDebug() << pd.m_tx.extra; + qDebug() << pd.m_tx.version; + t->m_prefixHash = QString::fromStdString(epee::string_tools::pod_to_hex(cryptonote::get_transaction_prefix_hash(pd.m_tx))); + m_rows.append(t); } diff --git a/src/libwalletqt/Wallet.cpp b/src/libwalletqt/Wallet.cpp index 507b734..85f6fec 100644 --- a/src/libwalletqt/Wallet.cpp +++ b/src/libwalletqt/Wallet.cpp @@ -8,6 +8,7 @@ #include "AddressBook.h" #include "Coins.h" +#include "MultisigMessageStore.h" #include "Subaddress.h" #include "SubaddressAccount.h" #include "TransactionHistory.h" @@ -22,6 +23,8 @@ #include "model/SubaddressModel.h" #include "model/SubaddressAccountModel.h" #include "model/CoinsModel.h" +#include "model/MultisigMessageModel.h" +#include "model/MultisigIncomingTxModel.h" #include "utils/ScopeGuard.h" @@ -47,9 +50,11 @@ Wallet::Wallet(Monero::Wallet *wallet, QObject *parent) , m_subaddressAccount(new SubaddressAccount(this, wallet->getWallet(), this)) , m_refreshNow(false) , m_refreshEnabled(false) + , m_mmsRefreshEnabled(false) , m_scheduler(this) , m_useSSL(true) , m_coins(new Coins(this, wallet->getWallet(), this)) + , m_mmsStore(new MultisigMessageStore(this, wallet->getWallet(), this)) , m_storeTimer(new QTimer(this)) { m_walletListener = new WalletListenerImpl(this); @@ -60,6 +65,8 @@ Wallet::Wallet(Monero::Wallet *wallet, QObject *parent) m_subaddressModel = new SubaddressModel(this, m_subaddress); m_subaddressAccountModel = new SubaddressAccountModel(this, m_subaddressAccount); m_coinsModel = new CoinsModel(this, m_coins); + m_mmsModel = new MultisigMessageModel(this, m_mmsStore); + m_msIncomingTxModel = new MultisigIncomingTxModel(this, m_mmsStore); if (this->status() == Status_Ok) { startRefreshThread(); @@ -86,6 +93,9 @@ Wallet::Wallet(Monero::Wallet *wallet, QObject *parent) connect(m_subaddress, &Subaddress::corrupted, [this]{ emit keysCorrupted(); }); + + connect(m_mmsStore, &MultisigMessageStore::multisigInfoImported, m_coins, &Coins::refresh); + connect(m_mmsStore, &MultisigMessageStore::multisigInfoExported, m_coins, &Coins::refresh); } // #################### Status #################### @@ -128,6 +138,12 @@ bool Wallet::viewOnly() const { return m_wallet2->watch_only(); } +bool Wallet::isMultisig() const { + bool ready, multisig; + multisig = m_wallet2->multisig(&ready); + return multisig; +} + bool Wallet::isDeterministic() const { return m_wallet2->is_deterministic(); } @@ -342,6 +358,12 @@ QString Wallet::getSeed(const QString &seedOffset) const { return QString::fromStdString(std::string(seed.data(), seed.size())); } +QString Wallet::getMultisigSeed() const { + epee::wipeable_string seed; + m_wallet2->get_multisig_seed(seed, ""); + return QString::fromStdString(std::string(seed.data(), seed.size())); +} + qsizetype Wallet::seedLength() const { auto seedLength = this->getCacheAttribute("feather.seed").split(" ", Qt::SkipEmptyParts).length(); return seedLength ? seedLength : 25; @@ -420,47 +442,54 @@ void Wallet::startRefreshThread() const auto future = m_scheduler.run([this] { // Beware! This code does not run in the GUI thread. - constexpr const std::chrono::seconds refreshInterval{10}; constexpr const std::chrono::milliseconds intervalResolution{100}; auto last = std::chrono::steady_clock::now(); while (!m_scheduler.stopping()) { - if (m_refreshEnabled && (!isHwBacked() || isDeviceConnected())) + if ((!isHwBacked() || isDeviceConnected())) { const auto now = std::chrono::steady_clock::now(); const auto elapsed = now - last; - if (elapsed >= refreshInterval || m_refreshNow) + if (elapsed >= m_refreshInterval || m_refreshNow) { - m_refreshNow = false; + if (m_refreshEnabled) { + m_refreshNow = false; - // get daemonHeight and targetHeight - // daemonHeight and targetHeight will be 0 if call to get_info fails - quint64 daemonHeight = m_walletImpl->daemonBlockChainHeight(); - bool success = daemonHeight > 0; + // get daemonHeight and targetHeight + // daemonHeight and targetHeight will be 0 if call to get_info fails + quint64 daemonHeight = m_walletImpl->daemonBlockChainHeight(); + bool success = daemonHeight > 0; - quint64 targetHeight = 0; - if (success) { - targetHeight = m_walletImpl->daemonBlockChainTargetHeight(); - } - bool haveHeights = (daemonHeight > 0 && targetHeight > 0); - - emit heightsRefreshed(haveHeights, daemonHeight, targetHeight); - - // Don't call refresh function if we don't have the daemon and target height - // We do this to prevent to UI from getting confused about the amount of blocks that are still remaining - if (haveHeights) { - QMutexLocker locker(&m_asyncMutex); - - if (m_newWallet) { - // Set blockheight to daemonHeight for newly created wallets to speed up initial sync - m_walletImpl->setRefreshFromBlockHeight(daemonHeight); - m_newWallet = false; + quint64 targetHeight = 0; + if (success) { + targetHeight = m_walletImpl->daemonBlockChainTargetHeight(); } + bool haveHeights = (daemonHeight > 0 && targetHeight > 0); - m_walletImpl->refresh(); + emit heightsRefreshed(haveHeights, daemonHeight, targetHeight); + + // Don't call refresh function if we don't have the daemon and target height + // We do this to prevent to UI from getting confused about the amount of blocks that are still remaining + if (haveHeights) { + QMutexLocker locker(&m_asyncMutex); + + if (m_newWallet) { + // Set blockheight to daemonHeight for newly created wallets to speed up initial sync + m_walletImpl->setRefreshFromBlockHeight(daemonHeight); + m_newWallet = false; + } + + m_walletImpl->refresh(); + } + last = std::chrono::steady_clock::now(); + } + + if (m_mmsRefreshEnabled) + { + m_mmsStore->receiveMessages(); + last = std::chrono::steady_clock::now(); } - last = std::chrono::steady_clock::now(); } } @@ -539,6 +568,7 @@ void Wallet::onNewBlock(uint64_t walletHeight) { void Wallet::onUpdated() { this->updateBalance(); if (this->isSynchronized()) { +// m_mmsStore->exportMultisig(); this->refreshModels(); } } @@ -563,6 +593,11 @@ void Wallet::refreshModels() { m_history->refresh(); m_coins->refresh(); this->subaddress()->refresh(this->currentSubaddressAccount()); + m_mmsStore->refresh(); +} + +void Wallet::setRefreshInterval(qint64 seconds) { + m_refreshInterval = std::chrono::seconds{seconds}; } // #################### Hardware wallet #################### @@ -1023,6 +1058,13 @@ PendingTransaction * Wallet::loadSignedTxFromStr(const std::string &data) return result; } +PendingTransaction * Wallet::restoreMultisigTransaction(const std::string &data) +{ + Monero::PendingTransaction *ptImpl = m_walletImpl->restoreMultisigTransaction(data); + PendingTransaction *result = new PendingTransaction(ptImpl, this); + return result; +} + bool Wallet::submitTxFile(const QString &fileName) const { qDebug() << "Trying to submit " << fileName; @@ -1103,6 +1145,18 @@ CoinsModel* Wallet::coinsModel() const { return m_coinsModel; } +MultisigMessageStore* Wallet::mmsStore() const { + return m_mmsStore; +} + +MultisigMessageModel* Wallet::mmsModel() const { + return m_mmsModel; +} + +MultisigIncomingTxModel* Wallet::msIncomingTxModel() const { + return m_msIncomingTxModel; +} + // #################### Transaction proofs #################### QString Wallet::getTxKey(const QString &txid) const { @@ -1287,6 +1341,55 @@ QString Wallet::make_uri(const QString &address, quint64 &amount, const QString return QString::fromStdString(uri); } +// #################### Multisig #################### + +void Wallet::setMMSRefreshEnabled(bool enabled) { + m_mmsRefreshEnabled = enabled; +} + +bool Wallet::hasMultisigPartialKeyImages() { + return m_walletImpl->hasMultisigPartialKeyImages(); +} + +QStringList Wallet::getMultisigSigners() { + QStringList signers; + mms::message_store& ms = m_wallet2->get_message_store(); + quint32 authorizedSigners = ms.get_num_authorized_signers(); + for (uint32_t j = 1; j < authorizedSigners; ++j) + { + mms::authorized_signer signer = ms.get_signer(j); + signers << QString::fromStdString(ms.signer_to_string(signer, 50));; + } + return signers; +} + +QString Wallet::getMultisigSignerConfig() { + mms::message_store& ms = m_wallet2->get_message_store(); + std::string signer_config; + ms.get_signer_config(signer_config); + std::string hex_signer_config = epee::string_tools::buff_to_hex_nodelimer(signer_config); + return QString::fromStdString(hex_signer_config); +} + +quint32 Wallet::multisigSigners() { + return m_wallet2->get_multisig_signers(); +} + +quint32 Wallet::multisigThreshold() { + mms::message_store& ms = m_wallet2->get_message_store(); + quint32 authorizedSigners = ms.get_num_required_signers(); + return authorizedSigners; +} + +//QString Wallet::originalPrimaryAddress() { +//// m_wallet2->m_original_address +// return ""; +//} +// +//QString Wallet::originalSecretViewkey() { +// return QString::fromStdString(epee::string_tools::pod_to_hex(m_wallet2->m_original_view_secret_key)); +//} + // #################### Misc / Unused #################### quint64 Wallet::getBytesReceived() const { diff --git a/src/libwalletqt/Wallet.h b/src/libwalletqt/Wallet.h index 7dbc038..3d3f3fe 100644 --- a/src/libwalletqt/Wallet.h +++ b/src/libwalletqt/Wallet.h @@ -71,6 +71,9 @@ class SubaddressAccount; class SubaddressAccountModel; class Coins; class CoinsModel; +class MultisigMessageStore; +class MultisigMessageModel; +class MultisigIncomingTxModel; struct TxProofResult { TxProofResult() {} @@ -133,6 +136,8 @@ public: //! returns if view only wallet bool viewOnly() const; + bool isMultisig() const; + //! return true if deterministic keys bool isDeterministic() const; @@ -175,6 +180,7 @@ public: //! returns mnemonic seed QString getSeed(const QString &seedOffset) const; + QString getMultisigSeed() const; qsizetype seedLength() const; @@ -227,6 +233,8 @@ public: void refreshModels(); + void setRefreshInterval(qint64 seconds); + // ##### Hardware wallet ##### bool isHwBacked() const; bool isLedger() const; @@ -342,6 +350,8 @@ public: PendingTransaction * loadSignedTxFile(const QString &fileName); PendingTransaction * loadSignedTxFromStr(const std::string &data); + PendingTransaction * restoreMultisigTransaction(const std::string &data); + //! Submit a transfer from file bool submitTxFile(const QString &fileName) const; @@ -359,6 +369,9 @@ public: SubaddressAccountModel* subaddressAccountModel() const; Coins* coins() const; CoinsModel* coinsModel() const; + MultisigMessageModel* mmsModel() const; + MultisigIncomingTxModel* msIncomingTxModel() const; + MultisigMessageStore* mmsStore() const; // ##### Transaction proofs ##### @@ -386,6 +399,20 @@ public: QString make_uri(const QString &address, quint64 &amount, const QString &description, const QString &recipient) const; + // ##### Multisig ##### + + void setMMSRefreshEnabled(bool enabled); + + bool hasMultisigPartialKeyImages(); + QStringList getMultisigSigners(); + + QString getMultisigSignerConfig(); + quint32 multisigSigners(); + quint32 multisigThreshold(); + +// QString originalPrimaryAddress(); +// QString originalSecretViewkey(); + // ##### Misc / Unused ##### quint64 getBytesReceived() const; @@ -498,6 +525,10 @@ private: Coins *m_coins; CoinsModel *m_coinsModel; + MultisigMessageStore *m_mmsStore; + MultisigMessageModel *m_mmsModel; + MultisigIncomingTxModel *m_msIncomingTxModel; + QMutex m_asyncMutex; QString m_daemonUsername; QString m_daemonPassword; @@ -505,6 +536,8 @@ private: QMutex m_proxyMutex; std::atomic m_refreshNow; std::atomic m_refreshEnabled; + std::atomic m_mmsRefreshEnabled; + std::chrono::seconds m_refreshInterval{10}; WalletListenerImpl *m_walletListener; FutureScheduler m_scheduler; diff --git a/src/libwalletqt/WalletListenerImpl.cpp b/src/libwalletqt/WalletListenerImpl.cpp index 5407be8..bf48dd4 100644 --- a/src/libwalletqt/WalletListenerImpl.cpp +++ b/src/libwalletqt/WalletListenerImpl.cpp @@ -4,6 +4,7 @@ #include "WalletListenerImpl.h" #include "Wallet.h" #include "WalletManager.h" +#include "MultisigMessageStore.h" WalletListenerImpl::WalletListenerImpl(Wallet * w) : m_wallet(w) @@ -31,6 +32,11 @@ void WalletListenerImpl::moneyReceived(const std::string &txId, uint64_t amount) QString qTxId = QString::fromStdString(txId); qDebug() << Q_FUNC_INFO << qTxId << " " << WalletManager::displayAmount(amount); + if (m_wallet->isMultisig()) { + // TODO: causes too many exports + m_wallet->mmsStore()->exportMultisig(); + } + emit m_wallet->moneyReceived(qTxId, amount); } diff --git a/src/libwalletqt/WalletManager.cpp b/src/libwalletqt/WalletManager.cpp index c34fea4..4fdf770 100644 --- a/src/libwalletqt/WalletManager.cpp +++ b/src/libwalletqt/WalletManager.cpp @@ -95,9 +95,12 @@ Wallet *WalletManager::openWallet(const QString &path, const QString &password, auto wallet = new Wallet(w); // move wallet to the GUI thread. Otherwise it wont be emitting signals + qDebug() << wallet->thread(); + qDebug() << qApp->thread(); if (wallet->thread() != qApp->thread()) { wallet->moveToThread(qApp->thread()); } + qDebug() << wallet->thread(); return wallet; } @@ -174,6 +177,16 @@ void WalletManager::createWalletFromDeviceAsync(const QString &path, const QStri }); } +Wallet* WalletManager::restoreMultisigWallet(const QString &path, const QString &password, NetworkType::Type nettype, + const QString &multisigSeed, const QString &mmsRecovery, quint64 restoreHeight, + quint64 kdfRounds, + const QString &subaddressLookahead) +{ + QMutexLocker locker(&m_mutex); + Monero::Wallet * w = m_pimpl->recoverMultisigWallet(path.toStdString(), password.toStdString(), static_cast(nettype), restoreHeight, multisigSeed.toStdString(), mmsRecovery.toStdString(), kdfRounds, subaddressLookahead.toStdString()); + return new Wallet(w); +} + bool WalletManager::walletExists(const QString &path) const { return m_pimpl->walletExists(path.toStdString()); diff --git a/src/libwalletqt/WalletManager.h b/src/libwalletqt/WalletManager.h index 596a03f..64d809f 100644 --- a/src/libwalletqt/WalletManager.h +++ b/src/libwalletqt/WalletManager.h @@ -58,6 +58,15 @@ public: const QString &subaddressLookahead = "" ); + Wallet * restoreMultisigWallet(const QString &path, + const QString &password, + NetworkType::Type nettype, + const QString &multisigSeed, + const QString &mmsRecovery, + quint64 restoreHeight = 0, + quint64 kdfRounds = 1, + const QString &subaddressLookahead = ""); + Wallet * createDeterministicWalletFromSpendKey(const QString &path, const QString &password, const QString &language, diff --git a/src/libwalletqt/rows/CoinsInfo.cpp b/src/libwalletqt/rows/CoinsInfo.cpp index 33d2449..0a89c55 100644 --- a/src/libwalletqt/rows/CoinsInfo.cpp +++ b/src/libwalletqt/rows/CoinsInfo.cpp @@ -124,6 +124,18 @@ QString CoinsInfo::txNote() const { return m_txNote; } +bool CoinsInfo::keyImagePartial() const { + return m_keyImagePartial; +} + +bool CoinsInfo::haveMultisigK() const { + return m_haveMultisigK; +} + +QStringList CoinsInfo::multisigInfo() const { + return m_multisigInfo; +} + CoinsInfo::CoinsInfo(QObject *parent) : QObject(parent) , m_blockHeight(0) @@ -142,6 +154,8 @@ CoinsInfo::CoinsInfo(QObject *parent) , m_unlocked(false) , m_coinbase(false) , m_change(false) + , m_keyImagePartial(false) + , m_haveMultisigK(false) { } diff --git a/src/libwalletqt/rows/CoinsInfo.h b/src/libwalletqt/rows/CoinsInfo.h index 9cee507..c59ef14 100644 --- a/src/libwalletqt/rows/CoinsInfo.h +++ b/src/libwalletqt/rows/CoinsInfo.h @@ -39,6 +39,9 @@ public: QString description() const; bool change() const; QString txNote() const; + bool keyImagePartial() const; + bool haveMultisigK() const; + QStringList multisigInfo() const; void setUnlocked(bool unlocked); @@ -71,6 +74,9 @@ private: QString m_description; bool m_change; QString m_txNote; + bool m_keyImagePartial; + bool m_haveMultisigK; + QStringList m_multisigInfo; }; #endif //FEATHER_COINSINFO_H diff --git a/src/libwalletqt/rows/MultisigMessage.cpp b/src/libwalletqt/rows/MultisigMessage.cpp new file mode 100644 index 0000000..82a3664 --- /dev/null +++ b/src/libwalletqt/rows/MultisigMessage.cpp @@ -0,0 +1,16 @@ +// +// Created by user on 1/3/24. +// + +#include "MultisigMessage.h" + + +MultisigMessage::MultisigMessage(QObject *parent) + : id(0) + , signer_index(0) + , wallet_height(0) + , round(0) + , signature_count(0) +{ + +} diff --git a/src/libwalletqt/rows/MultisigMessage.h b/src/libwalletqt/rows/MultisigMessage.h new file mode 100644 index 0000000..dfb8af9 --- /dev/null +++ b/src/libwalletqt/rows/MultisigMessage.h @@ -0,0 +1,56 @@ +// +// Created by user on 1/3/24. +// + +#ifndef FEATHER_MULTISIGMESSAGE_H +#define FEATHER_MULTISIGMESSAGE_H + +#include +#include + +//uint32_t id; +//message_type type; +//message_direction direction; +//std::string content; +//uint64_t created; +//uint64_t modified; +//uint64_t sent; +//uint32_t signer_index; +//crypto::hash hash; +//message_state state; +//uint32_t wallet_height; +//uint32_t round; +//uint32_t signature_count; +//std::string transport_id; + +class MultisigMessage : public QObject +{ + Q_OBJECT + +public: + quint32 id; + QString type; + QString direction; + std::string content; + QDateTime created; + QDateTime modified; + QDateTime sent; + quint32 signer_index; + QString signer; + QString hash; + QString state; + quint32 wallet_height; + quint32 round; + quint32 signature_count; + QString transport_id; + + +private: + explicit MultisigMessage(QObject *parent); + +private: + friend class MultisigMessageStore; +}; + + +#endif //FEATHER_MULTISIGMESSAGE_H diff --git a/src/libwalletqt/rows/TransactionRow.cpp b/src/libwalletqt/rows/TransactionRow.cpp index a4c1b7f..78c1933 100644 --- a/src/libwalletqt/rows/TransactionRow.cpp +++ b/src/libwalletqt/rows/TransactionRow.cpp @@ -164,4 +164,8 @@ QString TransactionRow::rings_formatted() const rings += "\n\n"; } return rings; +} + +QString TransactionRow::prefixHash() const { + return m_prefixHash; } \ No newline at end of file diff --git a/src/libwalletqt/rows/TransactionRow.h b/src/libwalletqt/rows/TransactionRow.h index 3fb8be4..a4eaa4e 100644 --- a/src/libwalletqt/rows/TransactionRow.h +++ b/src/libwalletqt/rows/TransactionRow.h @@ -50,6 +50,7 @@ public: QList destinations() const; QList transfers() const; QString rings_formatted() const; + QString prefixHash() const; private: explicit TransactionRow(); @@ -76,6 +77,7 @@ private: QDateTime m_timestamp; quint64 m_unlockTime; bool m_coinbase; + QString m_prefixHash; }; diff --git a/src/libwalletqt/rows/TxProposal.cpp b/src/libwalletqt/rows/TxProposal.cpp new file mode 100644 index 0000000..2d14526 --- /dev/null +++ b/src/libwalletqt/rows/TxProposal.cpp @@ -0,0 +1,27 @@ +// +// Created by user on 3/20/24. +// + +#include "TxProposal.h" + +TxProposal::TxProposal(QObject *parent) + : QObject(parent) + , messageId(0) + , txCount(0) + , numSignatures(0) +{ + +} + +QString TxProposal::errorString() { + if (this->txCount == 0) { + return {"Tx proposal contains no transactions"}; + } + + if (this->txCount > 1) { + // the UI doesn't support split transactions + return {"Tx proposal contains too many transactions"}; + } + + return ""; +} \ No newline at end of file diff --git a/src/libwalletqt/rows/TxProposal.h b/src/libwalletqt/rows/TxProposal.h new file mode 100644 index 0000000..60cef8b --- /dev/null +++ b/src/libwalletqt/rows/TxProposal.h @@ -0,0 +1,47 @@ +// +// Created by user on 3/20/24. +// + +#ifndef FEATHER_TXPROPOSAL_H +#define FEATHER_TXPROPOSAL_H + +#include +#include + +class TxProposal : public QObject +{ + Q_OBJECT + +public: + enum Status { + Pending = 0, + Expired, + Cant_Sign, + Signed, + Completed, + Frozen, + Double_Spend + }; + + quint64 messageId; + QDateTime timestamp; + Status status; + QStringList spendsOutputs; + quint64 txCount; + QString txId; + quint64 numSignatures; + QStringList signers; + QString from; + qint64 balanceDelta; + QString prefixHash; + + QString errorString(); + +private: + explicit TxProposal(QObject *parent); + +private: + friend class MultisigMessageStore; +}; + +#endif //FEATHER_TXPROPOSAL_H diff --git a/src/main.cpp b/src/main.cpp index 5f45f40..f40f99e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,7 +5,7 @@ #include #include #include -#include +//#include #include "config-feather.h" #include "constants.h" @@ -88,6 +88,11 @@ if (AttachConsole(ATTACH_PARENT_PROCESS)) { qputenv("QT_QPA_PLATFORM", "windows:darkmode=1"); #endif +// Force XCB to deal with 'Could not find the Qt platform plugin "wayland" in ""' +#if defined(Q_OS_LINUX) && defined(STATIC) + qputenv("QT_QPA_PLATFORM", "xcb"); +#endif + QStringList argv_; for(int i = 0; i != argc; i++){ argv_ << QString::fromStdString(argv[i]); @@ -137,7 +142,7 @@ if (AttachConsole(ATTACH_PARENT_PROCESS)) { QApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::Round); #endif - SingleApplication app(argc, argv); + QApplication app(argc, argv); QApplication::setQuitOnLastWindowClosed(false); QApplication::setApplicationName("FeatherWallet"); @@ -243,10 +248,11 @@ if (AttachConsole(ATTACH_PARENT_PROCESS)) { auto wm = windowManager(); wm->setEventFilter(&filter); + wm->raise(); - QObject::connect(&app, &SingleApplication::instanceStarted, [&wm]() { - wm->raise(); - }); +// QObject::connect(&app, &SingleApplication::instanceStarted, [&wm]() { +// wm->raise(); +// }); return QApplication::exec(); } diff --git a/src/model/CoinsModel.cpp b/src/model/CoinsModel.cpp index 03e4813..2b3f809 100644 --- a/src/model/CoinsModel.cpp +++ b/src/model/CoinsModel.cpp @@ -90,12 +90,22 @@ QVariant CoinsModel::data(const QModelIndex &index, int role) const switch (index.column()) { case KeyImageKnown: { - if (cInfo.keyImageKnown()) { - result = QVariant(icons()->icon("eye1.png")); - } - else { + if (!cInfo.keyImageKnown() || cInfo.keyImagePartial()) { result = QVariant(icons()->icon("eye_blind.png")); } + else { + result = QVariant(icons()->icon("eye1.png")); + } + break; + } + case HaveMultisigK: + { + if (cInfo.haveMultisigK()) { + result = QVariant(icons()->icon("status_connected.svg")); + } else { + result = QVariant(icons()->icon("status_lagging.svg")); + } + break; } } } @@ -116,6 +126,21 @@ QVariant CoinsModel::data(const QModelIndex &index, int role) const } else { result = "Key image unknown. Outgoing transactions that include this output will not be detected."; } + break; + } + case HaveMultisigK: + { + if (cInfo.haveMultisigK()) { + result = "We can spend this output in a transaction proposal."; + } else { + result = "We have recently spent this output in a transaction proposal."; + } + break; + } + case MultisigInfo: + { + result = cInfo.multisigInfo().join(", ") + " can sign a transaction that spends this output"; + break; } } if (cInfo.frozen()) { @@ -148,6 +173,8 @@ QVariant CoinsModel::headerData(int section, Qt::Orientation orientation, int ro switch(section) { case PubKey: return QString("Pub Key"); + case MultisigInfo: + return QString("Multisig info"); case TxID: return QString("TxID"); case BlockHeight: @@ -217,6 +244,10 @@ QVariant CoinsModel::parseTransactionInfo(const CoinsInfo &cInfo, int column, in { case KeyImageKnown: return ""; + case HaveMultisigK: + return ""; + case MultisigInfo: + return cInfo.multisigInfo().join(", "); case PubKey: return cInfo.pubKey().mid(0,8); case TxID: diff --git a/src/model/CoinsModel.h b/src/model/CoinsModel.h index d42bbba..859347a 100644 --- a/src/model/CoinsModel.h +++ b/src/model/CoinsModel.h @@ -22,6 +22,8 @@ public: enum ModelColumn { KeyImageKnown = 0, + HaveMultisigK, + MultisigInfo, PubKey, TxID, Address, diff --git a/src/model/MultisigIncomingTxModel.cpp b/src/model/MultisigIncomingTxModel.cpp new file mode 100644 index 0000000..3f665d2 --- /dev/null +++ b/src/model/MultisigIncomingTxModel.cpp @@ -0,0 +1,181 @@ +//// +//// Created by user on 1/9/24. +//// +// + +#include "MultisigIncomingTxModel.h" +#include "libwalletqt/rows/MultisigMessage.h" +#include "MultisigMessageStore.h" +#include "constants.h" +#include "utils/ColorScheme.h" +#include "utils/Icons.h" +#include "utils/Utils.h" +#include "libwalletqt/WalletManager.h" +#include "rows/TxProposal.h" +#include "config.h" + +#include + +MultisigIncomingTxModel::MultisigIncomingTxModel(QObject *parent, MultisigMessageStore *store) + : QAbstractTableModel(parent) + , m_store(store) +{ + connect(m_store, &MultisigMessageStore::refreshStarted, this, &MultisigIncomingTxModel::startReset); + connect(m_store, &MultisigMessageStore::refreshFinished, this, &MultisigIncomingTxModel::endReset); +} + +void MultisigIncomingTxModel::startReset(){ + beginResetModel(); +} + +void MultisigIncomingTxModel::endReset(){ + endResetModel(); +} + +int MultisigIncomingTxModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } else { + return m_store->txProposalCount(); + } +} + +int MultisigIncomingTxModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return ModelColumn::COUNT; +} + +QVariant MultisigIncomingTxModel::data(const QModelIndex &index, int role) const +{ + if (!m_store) { + return QVariant(); + } + + if (!index.isValid() || index.row() < 0 || static_cast(index.row()) >= m_store->count()) + return QVariant(); + + QVariant result; + + bool found = m_store->txProposal(index.row(), [this, &index, &result, &role](const TxProposal &msg) { + if(role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::UserRole) { + result = getMessageInfo(msg, index.column(), role); + } + else if (role == Qt::DecorationRole) { + switch (index.column()) { + case ModelColumn::Status: + { + switch (msg.status) { + case TxProposal::Status::Completed: { + result = QVariant(icons()->icon("confirmed.svg")); + break; + } + case TxProposal::Status::Cant_Sign: + case TxProposal::Status::Double_Spend: + { + result = QVariant(icons()->icon("expired.png")); + break; + } + case TxProposal::Status::Frozen: { + result = QVariant(icons()->icon("freeze.png")); + break; + } + case TxProposal::Status::Signed: { + result = QVariant(icons()->icon("sign.png")); + break; + } + default: { + result = QVariant(icons()->icon("arrow.svg")); + } + } + } + } + } + + }); + if (!found) { + qCritical("%s: internal error: no transaction info for index %d", __FUNCTION__, index.row()); + } + return result; +} + +QVariant MultisigIncomingTxModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole) { + return QVariant(); + } + if (orientation == Qt::Horizontal) + { + switch(section) { + case Status: + return QString("Status"); + case Date: + return QString("Date"); + case Signatures: + return QString("Signatures"); + case SpendsOutputs: + return QString("Spends Outputs"); + case TxCount: + return QString("Tx Count"); + case TxId: + return QString("Txid"); + case PrefixHash: + return QString("Prefix hash"); + case Amount: + return QString("Amount"); + } + } + return QVariant(); +} + + +QVariant MultisigIncomingTxModel::getMessageInfo(const TxProposal &msg, int column, int role) const +{ + switch (column) + { + case Status: { + switch (msg.status) { + case TxProposal::Status::Completed: + return "Complete"; + case TxProposal::Status::Cant_Sign: + return "Can't sign"; + case TxProposal::Status::Pending: + return "Pending"; + case TxProposal::Status::Expired: + return "Expired"; + case TxProposal::Status::Frozen: + return "Frozen"; + case TxProposal::Status::Signed: + return "Signed"; + case TxProposal::Status::Double_Spend: + return "Double spend"; + } + } + case Date: + return msg.timestamp.toString(QString("%1 %2 ").arg(conf()->get(Config::dateFormat).toString(), + conf()->get(Config::timeFormat).toString())); + case Signatures: + return msg.numSignatures; + case MessageID: + return msg.messageId; + case SpendsOutputs: + return msg.spendsOutputs.join(", "); + case TxCount: + return msg.txCount; + case TxId: + return msg.txId.mid(0, 8); + case PrefixHash: + return msg.prefixHash.mid(0, 8); + case Amount: + return WalletManager::displayAmount(msg.balanceDelta); + default: + { + qCritical() << "Unimplemented role"; + return QVariant(); + } + } +} + diff --git a/src/model/MultisigIncomingTxModel.h b/src/model/MultisigIncomingTxModel.h new file mode 100644 index 0000000..dc4ed40 --- /dev/null +++ b/src/model/MultisigIncomingTxModel.h @@ -0,0 +1,56 @@ +//// +//// Created by user on 1/9/24. +//// +// +#ifndef FEATHER_MULTISIGINCOMINGTXMODEL_H +#define FEATHER_MULTISIGINCOMINGTXMODEL_H + +#include + +#include +#include +#include +#include + +class MultisigMessageStore; +class MultisigMessage; +class TxProposal; + +class MultisigIncomingTxModel : public QAbstractTableModel +{ +Q_OBJECT + +public: + enum ModelColumn + { + Status, + Date, + MessageID, + Signatures, + SpendsOutputs, + TxCount, + TxId, + PrefixHash, + Amount, + COUNT + }; + + explicit MultisigIncomingTxModel(QObject *parent, MultisigMessageStore *store); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + +public slots: + void startReset(); + void endReset(); + +private: + QVariant getMessageInfo(const TxProposal &msg, int column, int role) const; + + MultisigMessageStore *m_store; +}; + + +#endif //FEATHER_MULTISIGINCOMINGTXMODEL_H diff --git a/src/model/MultisigMessageModel.cpp b/src/model/MultisigMessageModel.cpp new file mode 100644 index 0000000..d298b1c --- /dev/null +++ b/src/model/MultisigMessageModel.cpp @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "libwalletqt/rows/MultisigMessage.h" +#include "MultisigMessageModel.h" +#include "MultisigMessageStore.h" +#include "constants.h" +#include "utils/ColorScheme.h" +#include "utils/Icons.h" +#include "utils/Utils.h" +#include "libwalletqt/WalletManager.h" + +#include + +MultisigMessageModel::MultisigMessageModel(QObject *parent, MultisigMessageStore *store) + : QAbstractTableModel(parent) + , m_store(store) +{ + connect(m_store, &MultisigMessageStore::refreshStarted, this, &MultisigMessageModel::startReset); + connect(m_store, &MultisigMessageStore::refreshFinished, this, &MultisigMessageModel::endReset); +} + +void MultisigMessageModel::startReset(){ + beginResetModel(); +} + +void MultisigMessageModel::endReset(){ + endResetModel(); +} + +int MultisigMessageModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } else { + return m_store->count(); + } +} + +int MultisigMessageModel::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return ModelColumn::COUNT; +} + +QVariant MultisigMessageModel::data(const QModelIndex &index, int role) const +{ + if (!m_store) { + return QVariant(); + } + + if (!index.isValid() || index.row() < 0 || static_cast(index.row()) >= m_store->count()) + return QVariant(); + + QVariant result; + + bool found = m_store->message(index.row(), [this, &index, &result, &role](const MultisigMessage &msg) { + if(role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::UserRole) { + result = getMessageInfo(msg, index.column(), role); + } + }); + if (!found) { + qCritical("%s: internal error: no transaction info for index %d", __FUNCTION__, index.row()); + } + return result; +} + +QVariant MultisigMessageModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole) { + return QVariant(); + } + if (orientation == Qt::Horizontal) + { + switch(section) { + case Id: + return QString("Id"); + case Type: + return QString("Type"); + case Direction: + return QString("Direction"); + case Created: + return QString("Created"); + case Modified: + return QString("Modified"); + case Sent: + return QString("Sent"); + case Signer: + return QString("Signer"); + case Hash: + return QString("Hash"); + case State: + return QString("State"); + case WalletHeight: + return QString("Wallet Height"); + case Round: + return QString("Round"); + case SignatureCount: + return QString("Signature Count"); + case TransportId: + return QString("Transport ID"); + default: + return QVariant(); + } + } + return QVariant(); +} + + +QVariant MultisigMessageModel::getMessageInfo(const MultisigMessage &msg, int column, int role) const +{ + switch (column) + { + case Id: + return msg.id; + case Type: + return msg.type; + case Direction: + return msg.direction; + case Created: + return msg.created; + case Modified: + return msg.modified; + case Sent: + return msg.sent; + case Signer: + return msg.signer; + case Hash: { + return msg.hash; + } + case State: + return msg.state; + case WalletHeight: + return msg.wallet_height; + case Round: + { + return msg.round; + } + case SignatureCount: + return msg.signature_count; + case TransportId: + return msg.transport_id; + default: + { + qCritical() << "Unimplemented role"; + return QVariant(); + } + } +} + diff --git a/src/model/MultisigMessageModel.h b/src/model/MultisigMessageModel.h new file mode 100644 index 0000000..47f6f83 --- /dev/null +++ b/src/model/MultisigMessageModel.h @@ -0,0 +1,58 @@ +// +// Created by user on 1/3/24. +// + +#ifndef FEATHER_MULTISIGMESSAGEMODEL_H +#define FEATHER_MULTISIGMESSAGEMODEL_H + +#include + +#include +#include +#include +#include + +class MultisigMessageStore; +class MultisigMessage; + +class MultisigMessageModel : public QAbstractTableModel +{ +Q_OBJECT + +public: + enum ModelColumn + { + Id = 0, + Type, + Direction, + Created, + Modified, + Sent, + Signer, + Hash, + State, + WalletHeight, + Round, + SignatureCount, + TransportId, + COUNT + }; + + explicit MultisigMessageModel(QObject *parent, MultisigMessageStore *store); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + +public slots: + void startReset(); + void endReset(); + +private: + QVariant getMessageInfo(const MultisigMessage &msg, int column, int role) const; + + MultisigMessageStore *m_store; +}; + +#endif //FEATHER_MULTISIGMESSAGEMODEL_H diff --git a/src/model/TransactionHistoryModel.cpp b/src/model/TransactionHistoryModel.cpp index 1cbc17b..fa0d678 100644 --- a/src/model/TransactionHistoryModel.cpp +++ b/src/model/TransactionHistoryModel.cpp @@ -165,6 +165,10 @@ QVariant TransactionHistoryModel::parseTransactionInfo(const TransactionRow &tIn amount = (tInfo.balanceDelta() < 0) ? amount : "+" + amount; return amount; } +// case Column::TxPrefixHash: +// { +// return tInfo.prefixHash().mid(0, 8); +// } case Column::TxID: { if (conf()->get(Config::historyShowFullTxid).toBool()) { return tInfo.hash(); @@ -217,6 +221,8 @@ QVariant TransactionHistoryModel::headerData(int section, Qt::Orientation orient return QString("Amount"); case Column::TxID: return QString("Txid"); +// case Column::TxPrefixHash: +// return QString("Prefix hash"); case Column::FiatAmount: return QString("Fiat"); default: diff --git a/src/utils/Utils.cpp b/src/utils/Utils.cpp index e625aea..cad565d 100644 --- a/src/utils/Utils.cpp +++ b/src/utils/Utils.cpp @@ -513,6 +513,14 @@ QString displayAddress(const QString& address, int sections, const QString& sep) return list.join(sep); } +QString chunkAddress(const QString& address) { + QStringList list; + for (int i = 0; i < 19; i+= 1) { + list << address.mid(i*5, 5); + } + return list.join(" "); +} + QTextCharFormat addressTextFormat(const SubaddressIndex &index, quint64 amount) { QTextCharFormat rec; if (index.isPrimary()) { @@ -684,4 +692,13 @@ QString formatSyncStatus(quint64 height, quint64 target, bool daemonSync) { return "Synchronized"; } + +QString formatRestoreHeight(Wallet *wallet) { + if (!wallet) { + return ""; + } + + QDateTime restoreDate = appData()->restoreHeights[constants::networkType]->heightToDate(wallet->getWalletCreationHeight()); + return QString("%1 (%2)").arg(QString::number(wallet->getWalletCreationHeight()), restoreDate.toString("yyyy-MM-dd")); +} } diff --git a/src/utils/Utils.h b/src/utils/Utils.h index e9a1fba..ef2d3da 100644 --- a/src/utils/Utils.h +++ b/src/utils/Utils.h @@ -89,6 +89,7 @@ namespace Utils void externalLinkWarning(QWidget *parent, const QString &url); QString displayAddress(const QString& address, int sections = 3, const QString & sep = " "); + QString chunkAddress(const QString& address); QTextCharFormat addressTextFormat(const SubaddressIndex &index, quint64 amount); QFont getMonospaceFont(); @@ -116,6 +117,7 @@ namespace Utils void clearLayout(QLayout *layout, bool deleteWidgets = true); QString formatSyncStatus(quint64 height, quint64 target, bool daemonSync = false); + QString formatRestoreHeight(Wallet *wallet); } #endif //FEATHER_UTILS_H diff --git a/src/utils/config.cpp b/src/utils/config.cpp index 8617109..493e660 100644 --- a/src/utils/config.cpp +++ b/src/utils/config.cpp @@ -21,6 +21,7 @@ static const QHash configStrings = { {Config::warnOnStagenet,{QS("warnOnStagenet"), true}}, {Config::warnOnTestnet,{QS("warnOnTestnet"), true}}, {Config::warnOnKiImport,{QS("warnOnKiImport"), true}}, + {Config::warnOnMultisigExperimental,{QS("warnOnMultisigExperimental"), true}}, {Config::logLevel,{QS("logLevel"), 0}}, {Config::homeWidget,{QS("homeWidget"), "ccs"}}, diff --git a/src/utils/config.h b/src/utils/config.h index 59c1810..04cb9d3 100644 --- a/src/utils/config.h +++ b/src/utils/config.h @@ -25,6 +25,7 @@ public: warnOnStagenet, warnOnTestnet, warnOnKiImport, + warnOnMultisigExperimental, homeWidget, donateBeg, diff --git a/src/widgets/MultisigSetupWidget.cpp b/src/widgets/MultisigSetupWidget.cpp new file mode 100644 index 0000000..dceb382 --- /dev/null +++ b/src/widgets/MultisigSetupWidget.cpp @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "MultisigSetupWidget.h" +#include "ui_MultisigSetupWidget.h" + +#include "ringct/rctOps.h" + +MultisigSetupWidget::MultisigSetupWidget(QWidget *parent) + : QWidget(parent) + , ui(new Ui::MultisigSetupWidget) +{ + ui->setupUi(this); + +// char setupKey[35]; +// +// auto key = rct::rct2sk(rct::skGen() +} + + +MultisigSetupWidget::~MultisigSetupWidget() = default; diff --git a/src/widgets/MultisigSetupWidget.h b/src/widgets/MultisigSetupWidget.h new file mode 100644 index 0000000..470cc7f --- /dev/null +++ b/src/widgets/MultisigSetupWidget.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef FEATHER_MULTISIGSETUPWIDGET_H +#define FEATHER_MULTISIGSETUPWIDGET_H + +#include +#include +#include + +#include "model/NodeModel.h" +#include "utils/nodes.h" + +namespace Ui { + class MultisigSetupWidget; +} + + +class MultisigSetupWidget : public QWidget +{ +Q_OBJECT + +public: + explicit MultisigSetupWidget(QWidget *parent = nullptr); + ~MultisigSetupWidget(); + + QScopedPointer ui; +}; + +#endif //FEATHER_MULTISIGSETUPWIDGET_H diff --git a/src/widgets/MultisigSetupWidget.ui b/src/widgets/MultisigSetupWidget.ui new file mode 100644 index 0000000..e0e35bf --- /dev/null +++ b/src/widgets/MultisigSetupWidget.ui @@ -0,0 +1,415 @@ + + + MultisigSetupWidget + + + + 0 + 0 + 720 + 361 + + + + Form + + + + + + 1 + + + + + + + <html><head/><body><p><span style=" font-weight:700;">Create setup key</span></p></body></html> + + + + + + + Click 'Next' if you already have a setup key. + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 10 + + + + + + + + + + Threshold: + + + + + + + Signers: + + + + + + + Setup key: + + + + + + + + + false + + + + + + + Copy + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + 1 + + + 16 + + + 2 + + + + + + + false + + + Number of signatures required to spend funds. + + + + + + + + + + 0 + 0 + + + + 2 + + + 16 + + + 3 + + + + + + + + 0 + 0 + + + + Generate + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + + + + <html><head/><body><p><span style=" font-weight:700;">Multisig setup</span></p></body></html> + + + + + + + + + Setup key: + + + + + + + + + + Channel: + + + + + + + + + + + + + + Username: + + + + + + + + + + Threshold: + + + + + + + + + ? + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Signers: + + + + + + + + + ? + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + <html><head/><body><p><span style=" font-weight:700;">Waiting for participants</span></p></body></html> + + + + + + + + Name + + + + + Address + + + + + + + + + + + + <html><head/><body><p><span style=" font-weight:700;">Multisig wallet created successfully</span></p></body></html> + + + + + + + + + Address: + + + + + + + + + + + + All participants must verify that the address matches via a secure channel. + + + + + + + Do not proceed until you are certain that every signer has the same address. + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Next + + + + + + + + + + diff --git a/src/widgets/TxDetailsSimple.cpp b/src/widgets/TxDetailsSimple.cpp index 1d21417..ebdc324 100644 --- a/src/widgets/TxDetailsSimple.cpp +++ b/src/widgets/TxDetailsSimple.cpp @@ -72,6 +72,20 @@ void TxDetailsSimple::setDetails(Wallet *wallet, PendingTransaction *tx, const Q ui->label_fee->setStyleSheet(ColorScheme::RED.asStylesheet(true)); ui->label_fee->setToolTip("Unrealistic fee. You may be connected to a malicious node."); } + +// if (wallet->isMultisig()) { +// ui->btn_ +// } + +// if (wallet->isMultisig()) { +// tx-> +// +// auto multisigState = wallet->multisig(); +// if (multisigState.isMultisig && m_signers.size() < multisigState.threshold) { +// throw runtime_error("Not enough signers to send multisig transaction"); +// } +// ui->label_signatures->setText() +// } } TxDetailsSimple::~TxDetailsSimple() = default; \ No newline at end of file diff --git a/src/widgets/TxDetailsSimple.ui b/src/widgets/TxDetailsSimple.ui index 275fb40..2e64a9c 100644 --- a/src/widgets/TxDetailsSimple.ui +++ b/src/widgets/TxDetailsSimple.ui @@ -6,8 +6,8 @@ 0 0 - 386 - 152 + 576 + 230 @@ -109,6 +109,67 @@ + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Signatures: + + + + + + + 1/? + + + + + + + + 0 + 0 + + + + Send to: + + + + + + + + 0 + 0 + + + + + + + diff --git a/src/wizard/PageCreateWalletType.ui b/src/wizard/PageCreateWalletType.ui new file mode 100644 index 0000000..073e12c --- /dev/null +++ b/src/wizard/PageCreateWalletType.ui @@ -0,0 +1,60 @@ + + + PageCreateWalletType + + + + 0 + 0 + 600 + 417 + + + + WizardPage + + + + + + Select wallet type to create: + + + + + + Standard + + + true + + + + + + + Multisig + + + + + + + + + + Qt::Vertical + + + + 20 + 275 + + + + + + + + + diff --git a/src/wizard/PageHardwareDevice.cpp b/src/wizard/PageHardwareDevice.cpp index 7fecf86..834d51f 100644 --- a/src/wizard/PageHardwareDevice.cpp +++ b/src/wizard/PageHardwareDevice.cpp @@ -16,14 +16,13 @@ PageHardwareDevice::PageHardwareDevice(WizardFields *fields, QWidget *parent) { ui->setupUi(this); - ui->combo_deviceType->addItem("Ledger Nano S (PLUS) / X", DeviceType::LEDGER); - ui->combo_deviceType->addItem("Trezor Model T / Safe 3", DeviceType::TREZOR); + this->setTitle("Restore from hardware device"); connect(ui->btnOptions, &QPushButton::clicked, this, &PageHardwareDevice::onOptionsClicked); } void PageHardwareDevice::initializePage() { - ui->radioNewWallet->setChecked(true); + ui->radio_create->setChecked(true); } int PageHardwareDevice::nextId() const { @@ -35,8 +34,13 @@ int PageHardwareDevice::nextId() const { } bool PageHardwareDevice::validatePage() { - m_fields->deviceType = static_cast(ui->combo_deviceType->currentData().toInt()); - m_fields->showSetRestoreHeightPage = ui->radioRestoreWallet->isChecked(); + if (ui->radio_ledger->isChecked()) { + m_fields->deviceType = DeviceType::LEDGER; + } else { + m_fields->deviceType = DeviceType::TREZOR; + } + + m_fields->showSetRestoreHeightPage = ui->radio_restore->isChecked(); return true; } diff --git a/src/wizard/PageHardwareDevice.ui b/src/wizard/PageHardwareDevice.ui index a4f51f6..4148f1c 100644 --- a/src/wizard/PageHardwareDevice.ui +++ b/src/wizard/PageHardwareDevice.ui @@ -15,15 +15,31 @@ - - - Select device type: + + + Select device type: + + + + + Ledger (Nano S, Nano S+, Nano X) + + + true + + + + + + + Trezor (Model T, Safe 3) + + + + - - - @@ -32,79 +48,31 @@ - - - Create a new wallet file from device + + + Does the wallet currently hold any funds? + + + + + No + + + true + + + + + + + Yes + + + + - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 40 - 20 - - - - - - - - Use this option if the keys on the device hold no funds. (i.e. this wallet was never used before) - - - true - - - - - - - - - Restore a wallet from device - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 40 - 20 - - - - - - - - If this option is selected, you will be asked specify a wallet creation date or restore height next. - - - true - - - - - diff --git a/src/wizard/PageKeyType.cpp b/src/wizard/PageKeyType.cpp new file mode 100644 index 0000000..d733603 --- /dev/null +++ b/src/wizard/PageKeyType.cpp @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "PageKeyType.h" +#include "WalletWizard.h" +#include "ui_PageKeyType.h" + +#include + + +PageKeyType::PageKeyType(WizardFields *fields, QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::PageKeyType) + , m_fields(fields) +{ + ui->setupUi(this); +// this->setTitle("Restore multisig wallet"); + this->setTitle("Recover wallet"); +} + +void PageKeyType::initializePage() { + +} + +int PageKeyType::nextId() const { + if (ui->radio_seed) { + return WalletWizard::Page_WalletRestoreSeed; + } else { + return WalletWizard::Page_WalletRestoreKeys; + } + + return 0; +} + +bool PageKeyType::validatePage() { + m_fields->clearFields(); + + return true; +} \ No newline at end of file diff --git a/src/wizard/PageKeyType.h b/src/wizard/PageKeyType.h new file mode 100644 index 0000000..28241dd --- /dev/null +++ b/src/wizard/PageKeyType.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef FEATHER_PAGEKEYTYPE_H +#define FEATHER_PAGEKEYTYPE_H + +#include +#include +#include + +#include "WalletWizard.h" + +namespace Ui { + class PageKeyType; +} + +class PageKeyType : public QWizardPage +{ +Q_OBJECT + +public: + explicit PageKeyType(WizardFields *fields, QWidget *parent = nullptr); + void initializePage() override; + bool validatePage() override; + int nextId() const override; + +private: + Ui::PageKeyType *ui; + WizardFields *m_fields; +}; + +#endif //FEATHER_PAGEKEYTYPE_H diff --git a/src/wizard/PageKeyType.ui b/src/wizard/PageKeyType.ui new file mode 100644 index 0000000..4b3ad98 --- /dev/null +++ b/src/wizard/PageKeyType.ui @@ -0,0 +1,67 @@ + + + PageKeyType + + + + 0 + 0 + 485 + 329 + + + + WizardPage + + + + + + Select key type: + + + + + + Seed + + + true + + + + + + + Spendkey (deterministic) + + + + + + + Spendkey + Viewkey (non-deterministic) + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + diff --git a/src/wizard/PageMenu.cpp b/src/wizard/PageMenu.cpp index 6292645..4fbfe95 100644 --- a/src/wizard/PageMenu.cpp +++ b/src/wizard/PageMenu.cpp @@ -21,13 +21,34 @@ PageMenu::PageMenu(WizardFields *fields, WalletKeysFilesModel *wallets, QWidget ui->label_version->setText(QString("Feather %1 — by dsc & tobtoht").arg(FEATHER_VERSION)); QString settingsSkin = conf()->get(Config::skin).toString(); + +// connect(ui->btn_create, &QPushButton::clicked, [this]{ +// m_fields->mode = WizardMode::CreateWallet; +// m_fields->modeText = "Create wallet"; +// m_nextPage = WalletWizard::Page_Recover; +// wizard()->button(QWizard::NextButton)->click(); +// }); +// +// connect(ui->btn_open, &QPushButton::clicked, [this]{ +// m_fields->mode = WizardMode::OpenWallet; +// m_fields->modeText = "Open wallet"; +// m_nextPage = WalletWizard::Page_OpenWallet; +// wizard()->button(QWizard::NextButton)->click(); +// }); +// +// connect(ui->btn_restore, &QPushButton::clicked, [this]{ +//// m_fields->mode = WizardMode::RecoverWallet; +// m_fields->modeText = "Restore wallet"; +// m_nextPage = WalletWizard::Page_Recover; +// wizard()->button(QWizard::NextButton)->click(); +// }); } void PageMenu::initializePage() { if (m_walletKeysFilesModel->rowCount() > 0) { - ui->radioOpen->setChecked(true); + ui->radio_open->setChecked(true); } else { - ui->radioCreate->setChecked(true); + ui->radio_create->setChecked(true); } // Don't show setup wizard again @@ -35,15 +56,19 @@ void PageMenu::initializePage() { } int PageMenu::nextId() const { - if (ui->radioCreate->isChecked()) + if (ui->radio_create->isChecked()) return WalletWizard::Page_CreateWalletSeed; - if (ui->radioOpen->isChecked()) + if (ui->radio_createMultisig->isChecked()) + return WalletWizard::Page_MultisigExperimentalWarning; + if (ui->radio_open->isChecked()) return WalletWizard::Page_OpenWallet; - if (ui->radioSeed->isChecked()) + if (ui->radio_restoreSeed->isChecked()) return WalletWizard::Page_WalletRestoreSeed; - if (ui->radioViewOnly->isChecked()) + if (ui->radio_restoreKeys->isChecked()) return WalletWizard::Page_WalletRestoreKeys; - if (ui->radioCreateFromDevice->isChecked()) + if (ui->radio_restoreMultisig->isChecked()) + return WalletWizard::Page_MultisigRestoreSeed; + if (ui->radio_restoreHardware->isChecked()) return WalletWizard::Page_HardwareDevice; return 0; } @@ -51,23 +76,31 @@ int PageMenu::nextId() const { bool PageMenu::validatePage() { m_fields->clearFields(); - if (ui->radioCreate->isChecked()) { + if (ui->radio_create->isChecked()) { m_fields->mode = WizardMode::CreateWallet; m_fields->modeText = "Create wallet"; } - if (ui->radioOpen->isChecked()) { + if (ui->radio_createMultisig->isChecked()) { + m_fields->mode = WizardMode::CreateMultisig; + m_fields->modeText = "Create multisig wallet"; + } + if (ui->radio_open->isChecked()) { m_fields->mode = WizardMode::OpenWallet; m_fields->modeText = "Open wallet"; } - if (ui->radioSeed->isChecked()) { + if (ui->radio_restoreSeed->isChecked()) { m_fields->mode = WizardMode::RestoreFromSeed; m_fields->modeText = "Restore wallet"; } - if (ui->radioViewOnly->isChecked()) { + if (ui->radio_restoreKeys->isChecked()) { m_fields->mode = WizardMode::RestoreFromKeys; m_fields->modeText = "Restore wallet"; } - if (ui->radioCreateFromDevice->isChecked()) { + if (ui->radio_restoreMultisig->isChecked()) { + m_fields->mode = WizardMode::RestoreMultisig; + m_fields->modeText = "Restore multisig wallet"; + } + if (ui->radio_restoreHardware->isChecked()) { m_fields->mode = WizardMode::CreateWalletFromDevice; m_fields->modeText = "Create from hardware device"; } diff --git a/src/wizard/PageMenu.h b/src/wizard/PageMenu.h index de18e06..54e9279 100644 --- a/src/wizard/PageMenu.h +++ b/src/wizard/PageMenu.h @@ -26,6 +26,8 @@ private: Ui::PageMenu *ui; WalletKeysFilesModel *m_walletKeysFilesModel; WizardFields *m_fields; + + WalletWizard::Page m_nextPage; }; #endif //FEATHER_WIZARDMENU_H diff --git a/src/wizard/PageMenu.ui b/src/wizard/PageMenu.ui index cc445ca..b2cf2a7 100644 --- a/src/wizard/PageMenu.ui +++ b/src/wizard/PageMenu.ui @@ -6,8 +6,8 @@ 0 0 - 617 - 463 + 610 + 465 @@ -15,63 +15,92 @@ - - - - 400 - 0 - - - - Select option: - - - - - - - Qt::ClickFocus - - - Create new wallet - - - - - - - Qt::ClickFocus - - - Open wallet file - - - - - - - Qt::ClickFocus - - - Restore wallet from seed - - - - - - - Qt::ClickFocus - - - Restore wallet from keys - - - - - - - Create wallet from hardware device + + + + + + + + Create a new.. + + + + + + + Standard wallet + + + true + + + + + + + Multisig wallet + + + + + + + Qt::Horizontal + + + + + + + Open wallet file + + + + + + + Qt::Horizontal + + + + + + + Restore wallet from.. + + + + + + + Seed + + + + + + + Keys + + + + + + + Multisig seed + + + + + + + Hardware device + + + + @@ -82,24 +111,17 @@ 20 - 40 + 73 - - - - - false - - - by dsc & tobtoht - - - - + + + TextLabel + + diff --git a/src/wizard/PageRecoverWallet.cpp b/src/wizard/PageRecoverWallet.cpp new file mode 100644 index 0000000..28af7e7 --- /dev/null +++ b/src/wizard/PageRecoverWallet.cpp @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "PageRecoverWallet.h" + + +#include "WalletWizard.h" +#include "PageMenu.h" +#include "ui_PageRecoverWallet.h" + +#include + +#include "config-feather.h" + +PageRecoverWallet::PageRecoverWallet(WizardFields *fields, QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::PageRecoverWallet) + , m_fields(fields) +{ + ui->setupUi(this); + + this->setTitle("Recover wallet"); +} + +void PageRecoverWallet::initializePage() { + ui->radio_standard->setChecked(true); + this->setTitle(m_fields->modeText); +} + +int PageRecoverWallet::nextId() const { + if (ui->radio_standard->isChecked()) + return WalletWizard::Page_KeyType; + if (ui->radio_viewOnly->isChecked()) + return WalletWizard::Page_WalletRestoreKeys; + if (ui->radio_multisig->isChecked()) + return WalletWizard::Page_MultisigRestoreSeed; + if (ui->radio_hardware->isChecked()) + return WalletWizard::Page_HardwareDevice; + return 0; +} diff --git a/src/wizard/PageRecoverWallet.h b/src/wizard/PageRecoverWallet.h new file mode 100644 index 0000000..cdc97dd --- /dev/null +++ b/src/wizard/PageRecoverWallet.h @@ -0,0 +1,33 @@ +// +// Created by user on 3/2/24. +// + +#ifndef FEATHER_PAGERECOVERWALLET_H +#define FEATHER_PAGERECOVERWALLET_H + +#include +#include +#include + +#include "WalletWizard.h" + +namespace Ui { + class PageRecoverWallet; +} + +class PageRecoverWallet : public QWizardPage +{ +Q_OBJECT + +public: + explicit PageRecoverWallet(WizardFields *fields, QWidget *parent = nullptr); + void initializePage() override; + int nextId() const override; + +private: + Ui::PageRecoverWallet *ui; + WizardFields *m_fields; +}; + + +#endif //FEATHER_PAGERECOVERWALLET_H diff --git a/src/wizard/PageRecoverWallet.ui b/src/wizard/PageRecoverWallet.ui new file mode 100644 index 0000000..b8d80ac --- /dev/null +++ b/src/wizard/PageRecoverWallet.ui @@ -0,0 +1,80 @@ + + + PageRecoverWallet + + + + 0 + 0 + 589 + 404 + + + + WizardPage + + + + + + + 0 + 0 + + + + Select wallet type: + + + + + + Standard + + + true + + + + + + + View-only + + + + + + + Multisig + + + + + + + Hardware wallet + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + diff --git a/src/wizard/PageSetPassword.cpp b/src/wizard/PageSetPassword.cpp index aeefa3d..1006a5f 100644 --- a/src/wizard/PageSetPassword.cpp +++ b/src/wizard/PageSetPassword.cpp @@ -13,29 +13,42 @@ PageSetPassword::PageSetPassword(WizardFields *fields, QWidget *parent) , m_fields(fields) { ui->setupUi(this); - this->setFinalPage(true); ui->frame_password->setInfo(icons()->icon("lock"), "Choose a password to encrypt your wallet keys."); connect(ui->widget_password, &PasswordSetWidget::passwordEntryChanged, [this]{ this->completeChanged(); }); - - this->setButtonText(QWizard::FinishButton, "Create/Open wallet"); } void PageSetPassword::initializePage() { +// bool multisig = ( || m_fields->mode == WizardMode::RestoreMultisig); + this->setFinalPage(m_fields->mode != WizardMode::CreateMultisig); +// this->setFinalPage(true); + this->setButtonText(QWizard::FinishButton, "Create/Open wallet"); + this->setButtonText(QWizard::CommitButton, "Next"); + this->setCommitPage(true); this->setTitle(m_fields->modeText); ui->widget_password->resetFields(); } bool PageSetPassword::validatePage() { m_fields->password = ui->widget_password->password(); - emit createWallet(); + + // Prevent double clicks from creating a wallet twice + if (!m_walletCreated) { + emit createWallet(); + m_walletCreated = true; + } + return true; } int PageSetPassword::nextId() const { + if (m_fields->mode == WizardMode::CreateMultisig) { + return WalletWizard::Page_MultisigCreateSetupKey; + } + return -1; } diff --git a/src/wizard/PageSetPassword.h b/src/wizard/PageSetPassword.h index 6c276be..45e172d 100644 --- a/src/wizard/PageSetPassword.h +++ b/src/wizard/PageSetPassword.h @@ -26,10 +26,12 @@ public: signals: void createWallet(); + void createMultisigWallet(); private: Ui::PageSetPassword *ui; + bool m_walletCreated = false; WizardFields *m_fields; }; diff --git a/src/wizard/PageWalletFile.cpp b/src/wizard/PageWalletFile.cpp index c8a78d8..2df9da8 100644 --- a/src/wizard/PageWalletFile.cpp +++ b/src/wizard/PageWalletFile.cpp @@ -120,6 +120,9 @@ QString PageWalletFile::defaultWalletName() { walletStr = QString("trezor_%1"); } } + else if (m_fields->mode == WizardMode::CreateMultisig || m_fields->mode == WizardMode::RestoreMultisig) { + walletStr = QString("multisig_%1"); + } walletName = walletStr.arg(count); count++; } while (this->walletPathExists(walletName)); diff --git a/src/wizard/PageWalletRestoreKeys.cpp b/src/wizard/PageWalletRestoreKeys.cpp index d5f025a..1664b83 100644 --- a/src/wizard/PageWalletRestoreKeys.cpp +++ b/src/wizard/PageWalletRestoreKeys.cpp @@ -77,6 +77,7 @@ PageWalletRestoreKeys::PageWalletRestoreKeys(WizardFields *fields, QWidget *pare } void PageWalletRestoreKeys::initializePage() { + ui->stackedWidget->setCurrentIndex(0); this->showInputLines(); } @@ -97,12 +98,18 @@ void PageWalletRestoreKeys::showInputLines() { ui->frame_viewKey->hide(); ui->frame_spendKey->show(); } - else { + else if (ui->combo_walletType->currentIndex() == walletType::Spendable_Nondeterministic){ ui->frame_address->show(); ui->frame_viewKey->show(); ui->frame_spendKey->show(); } + if (ui->combo_walletType->currentIndex() == walletType::Multisig) { + ui->stackedWidget->setCurrentIndex(1); + } else { + ui->stackedWidget->setCurrentIndex(0); + } + ui->line_address->setText(""); ui->line_viewkey->setText(""); ui->line_spendkey->setText(""); @@ -111,6 +118,12 @@ void PageWalletRestoreKeys::showInputLines() { bool PageWalletRestoreKeys::validatePage() { auto errStyle = "QLineEdit{border: 1px solid red;}"; + if (walletType() == walletType::Multisig) { + // TODO: validation + m_fields->multisigSeed = ui->multisigSeed->toPlainText(); + return true; + } + ui->line_address->setStyleSheet(""); ui->line_viewkey->setStyleSheet(""); ui->label_errorString->hide(); diff --git a/src/wizard/PageWalletRestoreKeys.h b/src/wizard/PageWalletRestoreKeys.h index 6eafe34..947cd7d 100644 --- a/src/wizard/PageWalletRestoreKeys.h +++ b/src/wizard/PageWalletRestoreKeys.h @@ -23,7 +23,8 @@ class PageWalletRestoreKeys : public QWizardPage enum walletType { ViewOnly = 0, Spendable = 1, - Spendable_Nondeterministic = 2 + Spendable_Nondeterministic = 2, + Multisig = 3, }; public: diff --git a/src/wizard/PageWalletRestoreKeys.ui b/src/wizard/PageWalletRestoreKeys.ui index 0e65cda..aaa7140 100644 --- a/src/wizard/PageWalletRestoreKeys.ui +++ b/src/wizard/PageWalletRestoreKeys.ui @@ -58,105 +58,167 @@ - - - QFrame::NoFrame + + + 0 - - QFrame::Raised - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - <html><head/><body><p>Secret <span style=" font-weight:600;">spend</span> key</p></body></html> - - - - - - - - - - - - - QFrame::NoFrame - - - QFrame::Raised - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - <html><head/><body><p>Secret <span style=" font-weight:600;">view</span> key</p></body></html> - - - - - - - - - - - - - QFrame::NoFrame - - - QFrame::Raised - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Primary address - - - - - - - + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + <html><head/><body><p>Secret <span style=" font-weight:600;">spend</span> key</p></body></html> + + + + + + + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + <html><head/><body><p>Secret <span style=" font-weight:600;">view</span> key</p></body></html> + + + + + + + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Primary address + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Multisig seed: + + + + + + + + diff --git a/src/wizard/WalletWizard.cpp b/src/wizard/WalletWizard.cpp index e60bfa8..d4c1b5c 100644 --- a/src/wizard/WalletWizard.cpp +++ b/src/wizard/WalletWizard.cpp @@ -19,17 +19,32 @@ #include "PageHardwareDevice.h" #include "PageNetworkProxy.h" #include "PageNetworkWebsocket.h" +#include "multisig/PageMultisigExperimentalWarning.h" +#include "multisig/PageMultisigCreateSetupKey.h" +#include "multisig/PageMultisigParticipants.h" +#include "multisig/PageMultisigOwnAddress.h" +#include "multisig/PageMultisigSignerInfo.h" +#include "multisig/PageMultisigSetupDebug.h" +#include "multisig/PageMultisigSeed.h" +#include "multisig/PageMultisigEnterSetupKey.h" +#include "multisig/PageMultisigEnterChannel.h" +#include "multisig/PageMultisigSignerConfig.h" +#include "multisig/PageMultisigSetupKey.h" +#include "multisig/PageMultisigEnterName.h" +#include "multisig/PageMultisigSetupWallet.h" +#include "multisig/PageMultisigVerifyAddress.h" +#include "multisig/PageMultisigRestoreSeed.h" +#include "multisig/PageMultisigMMSRecoveryInfo.h" +#include "multisig/PageMultisigRestoreMMSRecoveryInfo.h" #include "constants.h" #include "WindowManager.h" - -#include -#include -#include +#include "PageRecoverWallet.h" +#include "PageKeyType.h" WalletWizard::WalletWizard(QWidget *parent) : QWizard(parent) { - this->setWindowTitle("Welcome to Feather Wallet"); + this->setWindowTitle("Feather Wizard"); this->setWindowIcon(QIcon(":/assets/images/appicons/64x64.png")); m_walletKeysFilesModel = new WalletKeysFilesModel(this); @@ -45,6 +60,9 @@ WalletWizard::WalletWizard(QWidget *parent) auto walletSetPasswordPage = new PageSetPassword(&m_wizardFields, this); auto walletSetSeedPassphrasePage = new PageSetSeedPassphrase(&m_wizardFields, this); auto walletSetSubaddressLookaheadPage = new PageSetSubaddressLookahead(&m_wizardFields, this); + auto multisigSetupDebug = new PageMultisigSetupDebug(&m_wizardFields, this); + auto multisigSeed = new PageMultisigSeed(&m_wizardFields, this); + auto multisigRecoveryInfo = new PageMultisigMMSRecoveryInfo(&m_wizardFields, this); setPage(Page_Menu, menuPage); setPage(Page_WalletFile, createWallet); setPage(Page_OpenWallet, openWalletPage); @@ -60,6 +78,25 @@ WalletWizard::WalletWizard(QWidget *parent) setPage(Page_SetSeedPassphrase, walletSetSeedPassphrasePage); setPage(Page_SetSubaddressLookahead, walletSetSubaddressLookaheadPage); setPage(Page_Plugins, new PagePlugins(this)); + setPage(Page_MultisigExperimentalWarning, new PageMultisigExperimentalWarning(&m_wizardFields, this)); + setPage(Page_MultisigCreateSetupKey, new PageMultisigCreateSetupKey(&m_wizardFields, this)); + setPage(Page_MultisigParticipants, new PageMultisigParticipants(&m_wizardFields, this)); + setPage(Page_MultisigOwnAddress, new PageMultisigOwnAddress(&m_wizardFields, this)); + setPage(Page_MultisigSignerInfo, new PageMultisigSignerInfo(&m_wizardFields, this)); + setPage(Page_MultisigSetupDebug, multisigSetupDebug); + setPage(Page_MultisigSeed, multisigSeed); + setPage(Page_MultisigEnterSetupKey, new PageMultisigEnterSetupKey(&m_wizardFields, this)); + setPage(Page_MultisigEnterChannel, new PageMultisigEnterChannel(&m_wizardFields, this)); + setPage(Page_MultisigSignerConfig, new PageMultisigSignerConfig(&m_wizardFields, this)); + setPage(Page_MultisigShowSetupKey, new PageMultisigSetupKey(&m_wizardFields, this)); + setPage(Page_MultisigEnterName, new PageMultisigEnterName(&m_wizardFields, this)); + setPage(Page_MultisigSetupWallet, new PageMultisigSetupWallet(&m_wizardFields, this)); + setPage(Page_MultisigVerifyAddress, new PageMultisigVerifyAddress(&m_wizardFields, this)); + setPage(Page_Recover, new PageRecoverWallet(&m_wizardFields, this)); + setPage(Page_MultisigRestoreSeed, new PageMultisigRestoreSeed(&m_wizardFields, this)); + setPage(Page_KeyType, new PageKeyType(&m_wizardFields, this)); + setPage(Page_MultisigMMSRecoveryInfo, multisigRecoveryInfo); + setPage(Page_MultisigRestoreMMSRecoveryInfo, new PageMultisigRestoreMMSRecoveryInfo(&m_wizardFields, this)); setStartId(Page_Menu); @@ -80,6 +117,8 @@ WalletWizard::WalletWizard(QWidget *parent) layout << QWizard::CommitButton; this->setButtonLayout(layout); + this->setButtonText(QWizard::CommitButton, "Next"); + auto *settingsButton = new QPushButton("Settings", this); this->setButton(QWizard::CustomButton1, settingsButton); @@ -102,6 +141,11 @@ WalletWizard::WalletWizard(QWidget *parent) emit openWallet(path, ""); }); + connect(multisigRecoveryInfo, &PageMultisigMMSRecoveryInfo::showWallet, [this](Wallet* wallet){ + m_wizardFields.wallet = nullptr; + emit showWallet(wallet); + }); + connect(this, &QWizard::helpRequested, this, &WalletWizard::showHelp); } @@ -109,6 +153,10 @@ void WalletWizard::resetFields() { m_wizardFields = {}; } +void WalletWizard::setWallet(Wallet *wallet) { + m_wizardFields.wallet = wallet; +} + void WalletWizard::onCreateWallet() { auto walletPath = QString("%1/%2").arg(m_wizardFields.walletDir, m_wizardFields.walletName); @@ -147,6 +195,21 @@ void WalletWizard::onCreateWallet() { return; } + if (m_wizardFields.mode == WizardMode::CreateMultisig) { + // We didn't generate a seed, generate one here. + m_wizardFields.seed = Seed(Seed::Type::POLYSEED, constants::networkType, "English", nullptr); + } + + if (m_wizardFields.mode == WizardMode::RestoreMultisig) { + emit restoreMultisigWallet(walletPath, + m_wizardFields.password, + m_wizardFields.multisigSeed, + m_wizardFields.multisigMMSRecovery, + m_wizardFields.restoreHeight, + m_wizardFields.subaddressLookahead); + return; + } + // If we're connected to the websocket, use the reported height for new wallets to skip initial synchronization. if (m_wizardFields.mode == WizardMode::CreateWallet && currentBlockHeight > 0) { qInfo() << "New wallet, setting restore height to latest blockheight: " << currentBlockHeight; @@ -157,9 +220,10 @@ void WalletWizard::onCreateWallet() { m_wizardFields.seed.setRestoreHeight(m_wizardFields.restoreHeight); } - bool newWallet = m_wizardFields.mode == WizardMode::CreateWallet; + bool newWallet = (m_wizardFields.mode == WizardMode::CreateWallet || m_wizardFields.mode == WizardMode::CreateMultisig); - emit createWallet(m_wizardFields.seed, walletPath, m_wizardFields.password, m_wizardFields.seedLanguage, m_wizardFields.seedOffsetPassphrase, m_wizardFields.subaddressLookahead, newWallet); + bool giveToWizard = m_wizardFields.mode == WizardMode::CreateMultisig; + emit createWallet(m_wizardFields.seed, walletPath, m_wizardFields.password, m_wizardFields.seedLanguage, m_wizardFields.seedOffsetPassphrase, m_wizardFields.subaddressLookahead, newWallet, giveToWizard); } QString WalletWizard::helpPage() { @@ -195,4 +259,8 @@ void WalletWizard::showHelp() { if (!doc.isEmpty()) { windowManager()->showDocs(this, doc); } -} \ No newline at end of file +} + +WalletWizard::~WalletWizard() { + delete m_wizardFields.wallet; +} diff --git a/src/wizard/WalletWizard.h b/src/wizard/WalletWizard.h index 92cd77d..74125d9 100644 --- a/src/wizard/WalletWizard.h +++ b/src/wizard/WalletWizard.h @@ -19,7 +19,9 @@ enum WizardMode { OpenWallet, RestoreFromSeed, RestoreFromKeys, - CreateWalletFromDevice + CreateWalletFromDevice, + CreateMultisig, + RestoreMultisig }; enum DeviceType { @@ -27,6 +29,18 @@ enum DeviceType { TREZOR }; +enum SignerConfig { + AUTOMATIC = 0, + SEMI_AUTOMATIC, + MANUAL +}; + +struct MMSSigner { + quint32 index = 0; + QString label; + QString address; +}; + struct WizardFields { QString walletName; QString walletDir; @@ -46,6 +60,17 @@ struct WizardFields { Seed::Type seedType; DeviceType deviceType; QString subaddressLookahead; + bool multisigInitiator = false; + QString multisigSetupKey; + quint32 multisigThreshold = 0; + quint32 multisigSigners = 0; + bool multisigAutomaticSetup = true; + QString multisigUsername; + QString multisigService; + QString multisigChannel; + QString multisigSeed; + QString multisigMMSRecovery; + Wallet *wallet = nullptr; void clearFields() { showSetSeedPassphrasePage = false; @@ -58,6 +83,7 @@ struct WizardFields { secretSpendKey = ""; restoreHeight = 0; subaddressLookahead = ""; + wallet = nullptr; } WizardFields(): deviceType(DeviceType::LEDGER), mode(WizardMode::CreateWallet), @@ -78,26 +104,50 @@ public: Page_SetSubaddressLookahead, Page_OpenWallet, Page_Network, + Page_Recover, Page_WalletRestoreSeed, Page_WalletRestoreKeys, Page_SetRestoreHeight, Page_HardwareDevice, Page_NetworkProxy, Page_NetworkWebsocket, - Page_Plugins + Page_Plugins, + Page_MultisigExperimentalWarning, + Page_MultisigCreateSetupKey, + Page_MultisigParticipants, + Page_MultisigOwnAddress, + Page_MultisigSignerInfo, + Page_MultisigSetupDebug, + Page_MultisigSeed, + Page_MultisigEnterSetupKey, + Page_MultisigEnterChannel, + Page_MultisigSignerConfig, + Page_MultisigShowSetupKey, + Page_MultisigEnterName, + Page_MultisigSetupWallet, + Page_MultisigVerifyAddress, + Page_MultisigRestoreSeed, + Page_MultisigMMSRecoveryInfo, + Page_MultisigRestoreMMSRecoveryInfo, + Page_KeyType }; explicit WalletWizard(QWidget *parent = nullptr); + ~WalletWizard() override; + void resetFields(); + void setWallet(Wallet* wallet); signals: void initialNetworkConfigured(); void showSettings(); void openWallet(QString path, QString password); + void showWallet(Wallet *wallet); void createWalletFromDevice(const QString &path, const QString &password, const QString &deviceName, int restoreHeight, const QString &subaddressLookahead); void createWalletFromKeys(const QString &path, const QString &password, const QString &address, const QString &viewkey, const QString &spendkey, quint64 restoreHeight, const QString subaddressLookahead = ""); - void createWallet(Seed seed, const QString &path, const QString &password, const QString &seedLanguage, const QString &seedOffset, const QString &subaddressLookahead, bool newWallet); + void createWallet(Seed seed, const QString &path, const QString &password, const QString &seedLanguage, const QString &seedOffset, const QString &subaddressLookahead, bool newWallet, bool giveToWizard); + void restoreMultisigWallet(const QString &path, const QString &password, const QString &multisigSeed, const QString &mmsRecovery, quint64 restoreHeight, const QString &subaddressLookahead); private slots: void onCreateWallet(); diff --git a/src/wizard/multisig/PageMultisigCreateSetupKey.cpp b/src/wizard/multisig/PageMultisigCreateSetupKey.cpp new file mode 100644 index 0000000..d92ee9e --- /dev/null +++ b/src/wizard/multisig/PageMultisigCreateSetupKey.cpp @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "PageMultisigCreateSetupKey.h" +#include "ui_PageMultisigCreateSetupKey.h" + +#include + +#include "libwalletqt/MultisigMessageStore.h" +#include "utils/Icons.h" +#include "utils/Utils.h" + +#include "ringct/rctOps.h" +#include "string_tools.h" + +PageMultisigCreateSetupKey::PageMultisigCreateSetupKey(WizardFields *fields, QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::PageMultisigCreateSetupKey) + , m_fields(fields) +{ + ui->setupUi(this); + this->setTitle("Create or use setup key"); + ui->infoFrame->setInfo(icons()->icon("key.png"), "One cosigner creates a new setup key and shares it with other " + "cosigners over a secure channel (e.g. end-to-end encrypted group chat.)"); +} + +void PageMultisigCreateSetupKey::initializePage() { + // We may need to return to the wizard if it is closed before the multisig setup completes + // Set a cache attribute to indicate that the setup is in progress + m_fields->wallet->setCacheAttribute("feather.multisig_setup", "started"); + + m_fields->mode = WizardMode::CreateMultisig; +} + + +int PageMultisigCreateSetupKey::nextId() const { + if (ui->radio_createSetupKey->isChecked()) { + // We are creating a new setup key + return WalletWizard::Page_MultisigParticipants; + } + + // We already have a setup key + return WalletWizard::Page_MultisigEnterSetupKey; +} + +bool PageMultisigCreateSetupKey::validatePage() { + // Prevent double click on the previous page from going to the next page + if (!ui->radio_haveSetupKey->isChecked() && !ui->radio_createSetupKey->isChecked()) { + return false; + } + + m_fields->multisigInitiator = ui->radio_createSetupKey->isChecked(); + return true; +} diff --git a/src/wizard/multisig/PageMultisigCreateSetupKey.h b/src/wizard/multisig/PageMultisigCreateSetupKey.h new file mode 100644 index 0000000..e66882f --- /dev/null +++ b/src/wizard/multisig/PageMultisigCreateSetupKey.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef FEATHER_PAGEMULTISIGCREATESETUPKEY_H +#define FEATHER_PAGEMULTISIGCREATESETUPKEY_H + +#include + +#include "wizard/WalletWizard.h" + +namespace Ui { + class PageMultisigCreateSetupKey; +} + +class PageMultisigCreateSetupKey : public QWizardPage +{ +Q_OBJECT + +public: + explicit PageMultisigCreateSetupKey(WizardFields *fields, QWidget *parent = nullptr); + void initializePage() override; + bool validatePage() override; + [[nodiscard]] int nextId() const override; + +private: + Ui::PageMultisigCreateSetupKey *ui; + WizardFields *m_fields; +}; + + +#endif //FEATHER_PAGEMULTISIGCREATESETUPKEY_H diff --git a/src/wizard/multisig/PageMultisigCreateSetupKey.ui b/src/wizard/multisig/PageMultisigCreateSetupKey.ui new file mode 100644 index 0000000..d3b90d2 --- /dev/null +++ b/src/wizard/multisig/PageMultisigCreateSetupKey.ui @@ -0,0 +1,82 @@ + + + PageMultisigCreateSetupKey + + + + 0 + 0 + 603 + 536 + + + + WizardPage + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 10 + + + + + + + + I have a setup key + + + + + + + Create a new setup key + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + InfoFrame + QFrame +
components.h
+ 1 +
+
+ + +
diff --git a/src/wizard/multisig/PageMultisigEnterChannel.cpp b/src/wizard/multisig/PageMultisigEnterChannel.cpp new file mode 100644 index 0000000..9f2594f --- /dev/null +++ b/src/wizard/multisig/PageMultisigEnterChannel.cpp @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "PageMultisigEnterChannel.h" +#include "ui_PageMultisigEnterChannel.h" + +#include + +#include "utils/Icons.h" +#include "utils/Utils.h" + +#include "ringct/rctOps.h" +#include "string_tools.h" +#include "libwalletqt/MultisigMessageStore.h" + +PageMultisigEnterChannel::PageMultisigEnterChannel(WizardFields *fields, QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::PageMultisigEnterChannel) + , m_fields(fields) +{ + ui->setupUi(this); + this->setTitle("Create setup key (2/3)"); + + ui->infoFrame->setInfo(icons()->icon("mail.png"), "Enter the URL for the messaging service that all participants will use.\n\nMake sure everyone agrees to use this service."); + + connect(ui->check_authRequired, &QCheckBox::toggled, [this](bool checked){ + ui->frame_auth->setVisible(checked); + }); + + connect(ui->line_service, &QLineEdit::textChanged, [this] { + m_channelRegistered = false; + }); + + connect(ui->radio_no, &QRadioButton::toggled, [this] { + completeChanged(); + }); + connect(ui->radio_yes, &QRadioButton::toggled, [this] { + completeChanged(); + }); +} + +void PageMultisigEnterChannel::registerChannel() { + QString serviceUrl = ui->line_service->text(); + QString serviceLogin = ui->check_authRequired->isChecked() ? ui->line_apiKey->text() : ""; + + m_fields->wallet->mmsStore()->setServiceDetails(serviceUrl, serviceLogin); + + // TODO: make async + QString channel; + bool success = m_fields->wallet->mmsStore()->registerChannel(channel, m_fields->multisigSigners); + if (success) { + m_fields->multisigChannel = channel; + m_channelRegistered = true; + completeChanged(); + } else { + QString errorString = m_fields->wallet->mmsStore()->errorString(); + + if (errorString.contains("authentication")) { + ui->check_authRequired->setChecked(true); + } + + Utils::showError(this, "Unable to register channel", m_fields->wallet->mmsStore()->errorString()); + } +} + +void PageMultisigEnterChannel::initializePage() { + ui->frame_auth->hide(); + ui->frame_confirm->hide(); + ui->check_authRequired->setChecked(false); + + if (!m_fields->multisigInitiator) { + this->setTitle("Messaging service"); + ui->check_authRequired->hide(); + ui->line_service->setText(m_fields->multisigService); + ui->line_service->setReadOnly(true); + ui->infoFrame->setText("The initiator has chosen the following messaging service."); + ui->frame_confirm->show(); + + m_channelRegistered = true; + completeChanged(); + } +} + +int PageMultisigEnterChannel::nextId() const { + if (m_fields->mode == WizardMode::CreateMultisig) { + if (m_fields->multisigInitiator) { + return WalletWizard::Page_MultisigSignerConfig; + } + else { + return WalletWizard::Page_MultisigEnterName; + } + } + if (m_fields->mode == WizardMode::RestoreMultisig) { + return WalletWizard::Page_SetRestoreHeight; + } + + return -1; +} + +bool PageMultisigEnterChannel::validatePage() { + if (!m_channelRegistered) { + this->registerChannel(); + } + + if (!m_channelRegistered) { + return false; + } + + m_fields->multisigService = ui->line_service->text(); + + return true; +} + +bool PageMultisigEnterChannel::isComplete() const { + if (!m_fields->multisigInitiator) { + return ui->radio_yes->isChecked(); + } + + return true; +} + diff --git a/src/wizard/multisig/PageMultisigEnterChannel.h b/src/wizard/multisig/PageMultisigEnterChannel.h new file mode 100644 index 0000000..fa74f01 --- /dev/null +++ b/src/wizard/multisig/PageMultisigEnterChannel.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef FEATHER_PAGEMULTISIGENTERCHANNEL_H +#define FEATHER_PAGEMULTISIGENTERCHANNEL_H + +#include +#include +#include +#include + +#include "wizard/WalletWizard.h" + +namespace Ui { + class PageMultisigEnterChannel; +} + +class PageMultisigEnterChannel : public QWizardPage +{ +Q_OBJECT + +public: + explicit PageMultisigEnterChannel(WizardFields *fields, QWidget *parent = nullptr); + void initializePage() override; + bool validatePage() override; + int nextId() const override; + bool isComplete() const override; + +private slots: + void registerChannel(); + +private: + Ui::PageMultisigEnterChannel *ui; + WizardFields *m_fields; + + bool m_channelRegistered = false; +}; + + +#endif //FEATHER_PAGEMULTISIGENTERCHANNEL_H diff --git a/src/wizard/multisig/PageMultisigEnterChannel.ui b/src/wizard/multisig/PageMultisigEnterChannel.ui new file mode 100644 index 0000000..9535f94 --- /dev/null +++ b/src/wizard/multisig/PageMultisigEnterChannel.ui @@ -0,0 +1,147 @@ + + + PageMultisigEnterChannel + + + + 0 + 0 + 561 + 310 + + + + WizardPage + + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + + + + Messaging service: + + + + + + + http://ms.featherwallet.net:80/ + + + + + + + Qt::Orientation::Vertical + + + QSizePolicy::Policy::Fixed + + + + 20 + 5 + + + + + + + + This service requires authentication + + + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + + + API key: + + + + + + + + + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + + + Is this ok? + + + + + + + No + + + true + + + + + + + Yes + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 0 + + + + + + + + + InfoFrame + QFrame +
components.h
+ 1 +
+
+ + +
diff --git a/src/wizard/multisig/PageMultisigEnterName.cpp b/src/wizard/multisig/PageMultisigEnterName.cpp new file mode 100644 index 0000000..103d57e --- /dev/null +++ b/src/wizard/multisig/PageMultisigEnterName.cpp @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "PageMultisigEnterName.h" +#include "ui_PageMultisigEnterName.h" + +#include "utils/Icons.h" +#include "utils/Utils.h" + +#include "libwalletqt/MultisigMessageStore.h" + +PageMultisigEnterName::PageMultisigEnterName(WizardFields *fields, QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::PageMultisigEnterName) + , m_fields(fields) +{ + ui->setupUi(this); + this->setTitle("Enter your name"); + + ui->infoFrame->setInfo(icons()->icon("change_account.png"), "Enter your (user)name, so other participants " + "can identify you.\n\nYour name will not become known " + "to the messaging service."); + + // We can't change the service URL or username after the setup has started. + this->setCommitPage(true); + this->setButtonText(QWizard::CommitButton, "Next"); +} + +void PageMultisigEnterName::initializePage() { + ui->line_name->setText(""); +} + +int PageMultisigEnterName::nextId() const { + if (m_fields->multisigAutomaticSetup) { + return WalletWizard::Page_MultisigSetupWallet; + } + else { + return WalletWizard::Page_MultisigOwnAddress; + } +} + +bool PageMultisigEnterName::validatePage() { + QString username = ui->line_name->text(); + if (username.isEmpty()) { + Utils::showError(this, "Enter a name to continue"); + return false; + } + + m_fields->multisigUsername = username; + + // We now have all the information needed to init the MMS + if (m_fields->mode == WizardMode::CreateMultisig) { + m_fields->wallet->mmsStore()->init(m_fields->multisigSetupKey, username); + } + + return true; +} diff --git a/src/wizard/multisig/PageMultisigEnterName.h b/src/wizard/multisig/PageMultisigEnterName.h new file mode 100644 index 0000000..aba5790 --- /dev/null +++ b/src/wizard/multisig/PageMultisigEnterName.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef PAGEMULTISIGENTERNAME_H +#define PAGEMULTISIGENTERNAME_H + +#include + +#include "wizard/WalletWizard.h" + +namespace Ui { + class PageMultisigEnterName; +} + +class PageMultisigEnterName : public QWizardPage +{ + Q_OBJECT + +public: + explicit PageMultisigEnterName(WizardFields *fields, QWidget *parent = nullptr); + void initializePage() override; + bool validatePage() override; + int nextId() const override; + +private: + Ui::PageMultisigEnterName *ui; + WizardFields *m_fields; +}; + +#endif //PAGEMULTISIGENTERNAME_H diff --git a/src/wizard/multisig/PageMultisigEnterName.ui b/src/wizard/multisig/PageMultisigEnterName.ui new file mode 100644 index 0000000..2180387 --- /dev/null +++ b/src/wizard/multisig/PageMultisigEnterName.ui @@ -0,0 +1,62 @@ + + + PageMultisigEnterName + + + + 0 + 0 + 646 + 477 + + + + WizardPage + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + Your name: + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + InfoFrame + QFrame +
components.h
+ 1 +
+
+ + +
diff --git a/src/wizard/multisig/PageMultisigEnterSetupKey.cpp b/src/wizard/multisig/PageMultisigEnterSetupKey.cpp new file mode 100644 index 0000000..8b0efae --- /dev/null +++ b/src/wizard/multisig/PageMultisigEnterSetupKey.cpp @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "PageMultisigEnterSetupKey.h" +#include "ui_PageMultisigEnterSetupKey.h" + +#include "libwalletqt/MultisigMessageStore.h" +#include "utils/Icons.h" + +PageMultisigEnterSetupKey::PageMultisigEnterSetupKey(WizardFields *fields, QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::PageMultisigEnterSetupKey) + , m_fields(fields) +{ + ui->setupUi(this); + this->setTitle("Enter setup key"); + + ui->infoFrame->setInfo(icons()->icon("key.png"), "The setup key should not be shared with outsiders and only used once per participant."); + + connect(ui->line_setupKey, &QLineEdit::textChanged, this, &PageMultisigEnterSetupKey::checkSetupKey); + connect(ui->radio_yes, &QRadioButton::toggled, [this](bool toggled){ + completeChanged(); + }); +} + +void PageMultisigEnterSetupKey::checkSetupKey(const QString &setupKey) { + ui->frame_invalid->hide(); + ui->frame_verify->hide(); + + if (setupKey.isEmpty()) { + return; + } + + MultisigMessageStore::SetupKey key; + bool keyValid = m_fields->wallet->mmsStore()->checkSetupKey(setupKey, key); + + if (!keyValid) { + ui->frame_invalid->show(); + return; + } + + m_fields->multisigThreshold = key.threshold; + m_fields->multisigSigners = key.participants; + m_fields->multisigService = key.service; + m_fields->multisigAutomaticSetup = (key.mode == MultisigMessageStore::SetupMode::AUTOMATIC); + m_fields->multisigSetupKey = setupKey; + + ui->frame_verify->show(); + ui->label_verify->setText(QString("You are setting up a %1-of-%2 multisig wallet. Is that correct?").arg(QString::number(key.threshold), QString::number(key.participants))); +} + +void PageMultisigEnterSetupKey::initializePage() { + ui->frame_invalid->hide(); + ui->frame_verify->hide(); + ui->line_setupKey->setText(""); +} + +int PageMultisigEnterSetupKey::nextId() const { + return WalletWizard::Page_MultisigEnterChannel; +} + +bool PageMultisigEnterSetupKey::validatePage() { + m_fields->multisigSetupKey = ui->line_setupKey->text(); + return true; +} + +bool PageMultisigEnterSetupKey::isComplete() const { + if (ui->radio_yes->isChecked()) { + return true; + } + + return false; +} + diff --git a/src/wizard/multisig/PageMultisigEnterSetupKey.h b/src/wizard/multisig/PageMultisigEnterSetupKey.h new file mode 100644 index 0000000..bd7ae3e --- /dev/null +++ b/src/wizard/multisig/PageMultisigEnterSetupKey.h @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef FEATHER_PAGEMULTISIGENTERSETUPKEY_H +#define FEATHER_PAGEMULTISIGENTERSETUPKEY_H + +#include + +#include "wizard/WalletWizard.h" + +namespace Ui { + class PageMultisigEnterSetupKey; +} + +class PageMultisigEnterSetupKey : public QWizardPage +{ +Q_OBJECT + +public: + explicit PageMultisigEnterSetupKey(WizardFields *fields, QWidget *parent = nullptr); + void initializePage() override; + bool validatePage() override; + int nextId() const override; + bool isComplete() const override; + +private slots: + void checkSetupKey(const QString &setupKey); + +private: + Ui::PageMultisigEnterSetupKey *ui; + WizardFields *m_fields; +}; + + +#endif //FEATHER_PAGEMULTISIGENTERSETUPKEY_H diff --git a/src/wizard/multisig/PageMultisigEnterSetupKey.ui b/src/wizard/multisig/PageMultisigEnterSetupKey.ui new file mode 100644 index 0000000..24d82c7 --- /dev/null +++ b/src/wizard/multisig/PageMultisigEnterSetupKey.ui @@ -0,0 +1,120 @@ + + + PageMultisigEnterSetupKey + + + + 0 + 0 + 664 + 510 + + + + WizardPage + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + Setup key: + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + Invalid setup key entered. + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + You are about to create a n-of-m multisig wallet. Is that correct? + + + true + + + + + + + No + + + true + + + + + + + Yes + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + InfoFrame + QFrame +
components.h
+ 1 +
+
+ + +
diff --git a/src/wizard/multisig/PageMultisigExperimentalWarning.cpp b/src/wizard/multisig/PageMultisigExperimentalWarning.cpp new file mode 100644 index 0000000..27f50c0 --- /dev/null +++ b/src/wizard/multisig/PageMultisigExperimentalWarning.cpp @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "PageMultisigExperimentalWarning.h" +#include "ui_PageMultisigExperimentalWarning.h" + +#include + +#include "libwalletqt/MultisigMessageStore.h" +#include "utils/Icons.h" + +PageMultisigExperimentalWarning::PageMultisigExperimentalWarning(WizardFields *fields, QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::PageMultisigExperimentalWarning) + , m_fields(fields) +{ + ui->setupUi(this); + this->setTitle("Experimental Feature Warning"); + + ui->warningFrame->setInfo(icons()->icon("warning.png"), "This is an experimental feature."); + + ui->label_warning->setText("Monero's multisig implementation has not been audited.

" + "It might be fundamentally broken, which could result in a loss of funds.

" + "You are strongly advised to only test this feature with cosigners you trust."); + ui->label_warning->setTextFormat(Qt::RichText); + + connect(ui->check_confirm, &QCheckBox::clicked, [this](bool checked){ + completeChanged(); + }); +} + +void PageMultisigExperimentalWarning::initializePage() { + ui->check_confirm->setChecked(false); +} + +int PageMultisigExperimentalWarning::nextId() const { + return WalletWizard::Page_WalletFile; +} + +bool PageMultisigExperimentalWarning::validatePage() { + return true; +} + +bool PageMultisigExperimentalWarning::isComplete() const { + if (ui->check_confirm->isChecked()) { + return true; + } + + return false; +} + diff --git a/src/wizard/multisig/PageMultisigExperimentalWarning.h b/src/wizard/multisig/PageMultisigExperimentalWarning.h new file mode 100644 index 0000000..4d74b79 --- /dev/null +++ b/src/wizard/multisig/PageMultisigExperimentalWarning.h @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef FEATHER_PAGEMULTISIGEXPERIMENTALWARNING_H +#define FEATHER_PAGEMULTISIGEXPERIMENTALWARNING_H + +#include + +#include "wizard/WalletWizard.h" + +namespace Ui { + class PageMultisigExperimentalWarning; +} + +class PageMultisigExperimentalWarning : public QWizardPage +{ +Q_OBJECT + +public: + explicit PageMultisigExperimentalWarning(WizardFields *fields, QWidget *parent = nullptr); + void initializePage() override; + bool validatePage() override; + int nextId() const override; + bool isComplete() const override; + + Ui::PageMultisigExperimentalWarning *ui; + WizardFields *m_fields; +}; + + +#endif //FEATHER_PAGEMULTISIGEXPERIMENTALWARNING_H diff --git a/src/wizard/multisig/PageMultisigExperimentalWarning.ui b/src/wizard/multisig/PageMultisigExperimentalWarning.ui new file mode 100644 index 0000000..02cccaf --- /dev/null +++ b/src/wizard/multisig/PageMultisigExperimentalWarning.ui @@ -0,0 +1,97 @@ + + + PageMultisigExperimentalWarning + + + + 0 + 0 + 497 + 330 + + + + WizardPage + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + TextLabel + + + true + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + I understand. + + + + + + + + InfoFrame + QFrame +
components.h
+ 1 +
+
+ + +
diff --git a/src/wizard/multisig/PageMultisigMMSRecoveryInfo.cpp b/src/wizard/multisig/PageMultisigMMSRecoveryInfo.cpp new file mode 100644 index 0000000..576da3b --- /dev/null +++ b/src/wizard/multisig/PageMultisigMMSRecoveryInfo.cpp @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "PageMultisigMMSRecoveryInfo.h" +#include "ui_PageMultisigMMSRecoveryInfo.h" + +#include "utils/Icons.h" +#include "utils/Utils.h" + +#include "libwalletqt/MultisigMessageStore.h" + +PageMultisigMMSRecoveryInfo::PageMultisigMMSRecoveryInfo(WizardFields *fields, QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::PageMultisigMMSRecoveryInfo) + , m_fields(fields) +{ + ui->setupUi(this); + this->setTitle("MMS Recovery Info"); + + this->setFinalPage(true); + this->setButtonText(QWizard::FinishButton, "Open wallet"); + + ui->infoFrame->setInfo(icons()->icon("key.png"), "You will need this recovery info to reconnect to the messaging service if you need to restore your wallet.\n\nStore it safely alongside your seed."); + + connect(ui->check_saved, &QCheckBox::toggled, [this]{ + this->completeChanged(); + }); +} + +void PageMultisigMMSRecoveryInfo::initializePage() { +// QJsonDocument doc; +// QJsonObject obj; +// obj["restore_height"] = QString::number(m_fields->wallet->getWalletCreationHeight()).toInt(); +// obj["message_daemon"] = m_fields->multisigService; +// obj["setup_key"] = m_fields->multisigSetupKey; +// +// QJsonObject me; +// me["address"] = m_fields->originalPrimaryAddress; +// me["viewkey"] = m_fields->originalSecretViewKey; +// me["label"] = m_fields->multisigUsername; +// +// QJsonArray signers; +// signers.append(me); +// +// for (int i = 1; i < m_fields->wallet->multisigSigners(); i++) { +// QJsonObject signer; +// auto info = m_fields->wallet->mmsStore()->getSignerInfo(i); +// +// signer["address"] = info.address; +// signer["label"] = info.label; +// +// signers.append(signer); +// } +// +// obj["signers"] = signers; +// doc.setObject(obj); +// +// QString recoveryData = QString("MMS_RECOVERY:%1").arg(doc.toJson(QJsonDocument::Compact).toBase64()); + ui->mmsRecoveryInfo->setPlainText(m_fields->wallet->mmsStore()->getRecoveryInfo()); +} + +int PageMultisigMMSRecoveryInfo::nextId() const { + return -1; +} + +bool PageMultisigMMSRecoveryInfo::validatePage() { + emit showWallet(m_fields->wallet); + return true; +} + +bool PageMultisigMMSRecoveryInfo::isComplete() const { + return ui->check_saved->isChecked(); +} diff --git a/src/wizard/multisig/PageMultisigMMSRecoveryInfo.h b/src/wizard/multisig/PageMultisigMMSRecoveryInfo.h new file mode 100644 index 0000000..f8da24c --- /dev/null +++ b/src/wizard/multisig/PageMultisigMMSRecoveryInfo.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef FEATHER_PAGEMULTISIGMMSRECOVERYINFO_H +#define FEATHER_PAGEMULTISIGMMSRECOVERYINFO_H + +#include +#include +#include + +#include "wizard/WalletWizard.h" + +namespace Ui { + class PageMultisigMMSRecoveryInfo; +} + +class PageMultisigMMSRecoveryInfo : public QWizardPage +{ +Q_OBJECT + +public: + explicit PageMultisigMMSRecoveryInfo(WizardFields *fields, QWidget *parent = nullptr); + void initializePage() override; + bool validatePage() override; + int nextId() const override; + bool isComplete() const override; + +signals: + void showWallet(Wallet *wallet); + +private: + Ui::PageMultisigMMSRecoveryInfo *ui; + WizardFields *m_fields; +}; + + +#endif //FEATHER_PAGEMULTISIGMMSRECOVERYINFO_H diff --git a/src/wizard/multisig/PageMultisigMMSRecoveryInfo.ui b/src/wizard/multisig/PageMultisigMMSRecoveryInfo.ui new file mode 100644 index 0000000..2cf7d49 --- /dev/null +++ b/src/wizard/multisig/PageMultisigMMSRecoveryInfo.ui @@ -0,0 +1,80 @@ + + + PageMultisigMMSRecoveryInfo + + + + 0 + 0 + 648 + 570 + + + + WizardPage + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Save to file + + + + + + + Copy + + + + + + + + + I have saved my multisig seed and MMS recovery info. + + + + + + + + InfoFrame + QFrame +
components.h
+ 1 +
+
+ + +
diff --git a/src/wizard/multisig/PageMultisigOwnAddress.cpp b/src/wizard/multisig/PageMultisigOwnAddress.cpp new file mode 100644 index 0000000..dca8fce --- /dev/null +++ b/src/wizard/multisig/PageMultisigOwnAddress.cpp @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "PageMultisigOwnAddress.h" +#include "ui_PageMultisigOwnAddress.h" + +#include "utils/Icons.h" +#include "utils/Utils.h" + +PageMultisigOwnAddress::PageMultisigOwnAddress(WizardFields *fields, QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::PageMultisigOwnAddress) + , m_fields(fields) +{ + ui->setupUi(this); + this->setTitle("Your wallet address"); + + ui->infoFrame->setInfo(icons()->icon("tab_addresses.png"), "Copy the address below and send it to all co-signers.\n\nThe address is used to encrypt messages."); + + connect(ui->btn_copyAddress, &QPushButton::clicked, [this]{ + Utils::copyToClipboard(m_fields->wallet->address(0, 0)); + }); +} + +void PageMultisigOwnAddress::initializePage() { + ui->line_ownAddress->setPlainText(m_fields->wallet->address(0, 0)); +} + +int PageMultisigOwnAddress::nextId() const { + return WalletWizard::Page_MultisigSignerInfo; +} diff --git a/src/wizard/multisig/PageMultisigOwnAddress.h b/src/wizard/multisig/PageMultisigOwnAddress.h new file mode 100644 index 0000000..1a63f33 --- /dev/null +++ b/src/wizard/multisig/PageMultisigOwnAddress.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef FEATHER_PAGEMULTISIGOWNADDRESS_H +#define FEATHER_PAGEMULTISIGOWNADDRESS_H + +#include + +#include "wizard/WalletWizard.h" + +namespace Ui { + class PageMultisigOwnAddress; +} + +class PageMultisigOwnAddress : public QWizardPage +{ +Q_OBJECT + +public: + explicit PageMultisigOwnAddress(WizardFields *fields, QWidget *parent = nullptr); + void initializePage() override; + int nextId() const override; + +private: + Ui::PageMultisigOwnAddress *ui; + WizardFields *m_fields; +}; + +#endif //FEATHER_PAGEMULTISIGOWNADDRESS_H diff --git a/src/wizard/multisig/PageMultisigOwnAddress.ui b/src/wizard/multisig/PageMultisigOwnAddress.ui new file mode 100644 index 0000000..dda2f75 --- /dev/null +++ b/src/wizard/multisig/PageMultisigOwnAddress.ui @@ -0,0 +1,83 @@ + + + PageMultisigOwnAddress + + + + 0 + 0 + 710 + 353 + + + + WizardPage + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + true + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Copy + + + + + + + + + Qt::Vertical + + + + 20 + 83 + + + + + + + + + InfoFrame + QFrame +
components.h
+ 1 +
+
+ + +
diff --git a/src/wizard/multisig/PageMultisigParticipants.cpp b/src/wizard/multisig/PageMultisigParticipants.cpp new file mode 100644 index 0000000..9256b57 --- /dev/null +++ b/src/wizard/multisig/PageMultisigParticipants.cpp @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "PageMultisigParticipants.h" +#include "ui_PageMultisigParticipants.h" + +#include "utils/Icons.h" +#include "utils/Utils.h" + +PageMultisigParticipants::PageMultisigParticipants(WizardFields *fields, QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::PageMultisigParticipants) + , m_fields(fields) +{ + ui->setupUi(this); + this->setTitle("Create setup key (1/3)"); + + ui->infoFrame->setInfo(icons()->icon("sign.png"), "Choose the number of participants (cosigners) and the number of signatures required to spend funds."); + + connect(ui->spin_signers, &QSpinBox::valueChanged, [this](int value){ + ui->spin_threshold->setMaximum(value); + }); +} + + +void PageMultisigParticipants::initializePage() { + ui->spin_threshold->setValue(2); + ui->spin_signers->setValue(3); +} + +int PageMultisigParticipants::nextId() const { + return WalletWizard::Page_MultisigEnterChannel; +} + +bool PageMultisigParticipants::validatePage() { + int threshold = ui->spin_threshold->value(); + + if (threshold < 2) { + const auto button = QMessageBox::question(this, "Insecure multisig threshold", "Requiring only 1 signature means any cosigner can spend from this wallet.\n\nAre you sure you want to create this setup key?"); + if (button == QMessageBox::No) { + return false; + } + } + + m_fields->multisigThreshold = threshold; + m_fields->multisigSigners = ui->spin_signers->value(); + return true; +} diff --git a/src/wizard/multisig/PageMultisigParticipants.h b/src/wizard/multisig/PageMultisigParticipants.h new file mode 100644 index 0000000..802f704 --- /dev/null +++ b/src/wizard/multisig/PageMultisigParticipants.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef PAGEMULTISIGPARTICIPANTS_H +#define PAGEMULTISIGPARTICIPANTS_H + +#include +#include +#include +#include + +#include "wizard/WalletWizard.h" + +namespace Ui { + class PageMultisigParticipants; +} + +class PageMultisigParticipants : public QWizardPage +{ + Q_OBJECT + + public: + explicit PageMultisigParticipants(WizardFields *fields, QWidget *parent = nullptr); + void initializePage() override; + bool validatePage() override; + int nextId() const override; + + Ui::PageMultisigParticipants *ui; + WizardFields *m_fields; +}; + +#endif //PAGEMULTISIGPARTICIPANTS_H diff --git a/src/wizard/multisig/PageMultisigParticipants.ui b/src/wizard/multisig/PageMultisigParticipants.ui new file mode 100644 index 0000000..686bb36 --- /dev/null +++ b/src/wizard/multisig/PageMultisigParticipants.ui @@ -0,0 +1,146 @@ + + + PageMultisigParticipants + + + + 0 + 0 + 567 + 212 + + + + WizardPage + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 10 + + + + + + + + + + From + + + + + + + + + + 0 + 0 + + + + 2 + + + 16 + + + 3 + + + + + + + participants + + + + + + + + + Require + + + + + + + + + + 0 + 0 + + + + 1 + + + 3 + + + 2 + + + + + + + signatures to spend funds + + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + InfoFrame + QFrame +
components.h
+ 1 +
+
+ + +
diff --git a/src/wizard/multisig/PageMultisigRestoreMMSRecoveryInfo.cpp b/src/wizard/multisig/PageMultisigRestoreMMSRecoveryInfo.cpp new file mode 100644 index 0000000..8f94be0 --- /dev/null +++ b/src/wizard/multisig/PageMultisigRestoreMMSRecoveryInfo.cpp @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "PageMultisigRestoreMMSRecoveryInfo.h" +#include "ui_PageMultisigRestoreMMSRecoveryInfo.h" + +#include "WalletManager.h" + +PageMultisigRestoreMMSRecoveryInfo::PageMultisigRestoreMMSRecoveryInfo(WizardFields *fields, QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::PageMultisigRestoreMMSRecoveryInfo) + , m_fields(fields) +{ + ui->setupUi(this); + this->setTitle("Enter MMS recovery info"); + +} + +void PageMultisigRestoreMMSRecoveryInfo::initializePage() { + ui->mmsRecoveryInfo->setPlainText(""); +} + +int PageMultisigRestoreMMSRecoveryInfo::nextId() const { + return WalletWizard::Page_SetRestoreHeight; +} + +bool PageMultisigRestoreMMSRecoveryInfo::validatePage() { + m_fields->multisigMMSRecovery = ui->mmsRecoveryInfo->toPlainText(); + return true; +} diff --git a/src/wizard/multisig/PageMultisigRestoreMMSRecoveryInfo.h b/src/wizard/multisig/PageMultisigRestoreMMSRecoveryInfo.h new file mode 100644 index 0000000..68b16bf --- /dev/null +++ b/src/wizard/multisig/PageMultisigRestoreMMSRecoveryInfo.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef FEATHER_PAGEMULTISIGRESTOREMMSRECOVERYINFO_H +#define FEATHER_PAGEMULTISIGRESTOREMMSRECOVERYINFO_H + +#include +#include +#include + +#include "wizard/WalletWizard.h" + +namespace Ui { + class PageMultisigRestoreMMSRecoveryInfo; +} + +class PageMultisigRestoreMMSRecoveryInfo : public QWizardPage +{ +Q_OBJECT + +public: + explicit PageMultisigRestoreMMSRecoveryInfo(WizardFields *fields, QWidget *parent = nullptr); + void initializePage() override; + bool validatePage() override; + int nextId() const override; + +private: + Ui::PageMultisigRestoreMMSRecoveryInfo *ui; + WizardFields *m_fields; +}; + + +#endif //FEATHER_PAGEMULTISIGRESTOREMMSRECOVERYINFO_H diff --git a/src/wizard/multisig/PageMultisigRestoreMMSRecoveryInfo.ui b/src/wizard/multisig/PageMultisigRestoreMMSRecoveryInfo.ui new file mode 100644 index 0000000..0ba6bcb --- /dev/null +++ b/src/wizard/multisig/PageMultisigRestoreMMSRecoveryInfo.ui @@ -0,0 +1,38 @@ + + + PageMultisigRestoreMMSRecoveryInfo + + + + 0 + 0 + 656 + 581 + + + + WizardPage + + + + + + Enter MMS recovery info: + + + + + + + + + + Skip this step, do not use the MMS. + + + + + + + + diff --git a/src/wizard/multisig/PageMultisigRestoreSeed.cpp b/src/wizard/multisig/PageMultisigRestoreSeed.cpp new file mode 100644 index 0000000..070cfff --- /dev/null +++ b/src/wizard/multisig/PageMultisigRestoreSeed.cpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "PageMultisigRestoreSeed.h" +#include "ui_PageMultisigRestoreSeed.h" + +#include + +#include "wizard/WalletWizard.h" + +PageMultisigRestoreSeed::PageMultisigRestoreSeed(WizardFields *fields, QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::PageMultisigRestoreSeed) + , m_fields(fields) +{ + ui->setupUi(this); + this->setTitle("Restore multisig wallet"); + + connect(ui->btn_options, &QPushButton::clicked, this, &PageMultisigRestoreSeed::onOptionsClicked); +} + +void PageMultisigRestoreSeed::initializePage() { + ui->multisigSeed->setPlainText(""); +} + +int PageMultisigRestoreSeed::nextId() const { + return WalletWizard::Page_MultisigRestoreMMSRecoveryInfo; +} + +bool PageMultisigRestoreSeed::validatePage() { + m_fields->multisigSeed = ui->multisigSeed->toPlainText(); + return true; +} + +void PageMultisigRestoreSeed::onOptionsClicked() { + QDialog dialog(this); + dialog.setWindowTitle("Options"); + + QVBoxLayout layout; + QCheckBox check_subaddressLookahead("Set subaddress lookahead"); + check_subaddressLookahead.setChecked(m_fields->showSetSubaddressLookaheadPage); + + layout.addWidget(&check_subaddressLookahead); + QDialogButtonBox buttons(QDialogButtonBox::Ok); + layout.addWidget(&buttons); + dialog.setLayout(&layout); + connect(&buttons, &QDialogButtonBox::accepted, [&dialog]{ + dialog.close(); + }); + dialog.exec(); + + m_fields->showSetSubaddressLookaheadPage = check_subaddressLookahead.isChecked(); +} \ No newline at end of file diff --git a/src/wizard/multisig/PageMultisigRestoreSeed.h b/src/wizard/multisig/PageMultisigRestoreSeed.h new file mode 100644 index 0000000..49cb9ab --- /dev/null +++ b/src/wizard/multisig/PageMultisigRestoreSeed.h @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef FEATHER_PAGEMULTISIGRESTORESEED_H +#define FEATHER_PAGEMULTISIGRESTORESEED_H + +#include +#include +#include + +#include "wizard/WalletWizard.h" + +namespace Ui { + class PageMultisigRestoreSeed; +} + +class PageMultisigRestoreSeed : public QWizardPage +{ + Q_OBJECT + +public: + explicit PageMultisigRestoreSeed(WizardFields *fields, QWidget *parent = nullptr); + void initializePage() override; + bool validatePage() override; + int nextId() const override; + +private: + void onOptionsClicked(); + + Ui::PageMultisigRestoreSeed *ui; + WizardFields *m_fields; +}; + +#endif //FEATHER_PAGEMULTISIGRESTORESEED_H diff --git a/src/wizard/multisig/PageMultisigRestoreSeed.ui b/src/wizard/multisig/PageMultisigRestoreSeed.ui new file mode 100644 index 0000000..dcad1db --- /dev/null +++ b/src/wizard/multisig/PageMultisigRestoreSeed.ui @@ -0,0 +1,55 @@ + + + PageMultisigRestoreSeed + + + + 0 + 0 + 690 + 521 + + + + WizardPage + + + + + + Enter multisig seed: + + + + + + + + + + + + Options + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + diff --git a/src/wizard/multisig/PageMultisigSeed.cpp b/src/wizard/multisig/PageMultisigSeed.cpp new file mode 100644 index 0000000..21011c9 --- /dev/null +++ b/src/wizard/multisig/PageMultisigSeed.cpp @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "PageMultisigSeed.h" +#include "ui_PageMultisigSeed.h" + +#include + +#include "utils/Icons.h" +#include "utils/Utils.h" + +#include "ringct/rctOps.h" +#include "string_tools.h" + +#include "libwalletqt/Wallet.h" + +PageMultisigSeed::PageMultisigSeed(WizardFields *fields, QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::PageMultisigSeed) + , m_fields(fields) +{ + ui->setupUi(this); + this->setTitle("Multisig seed"); +// this->setCommitPage(true); + + ui->infoFrame->setInfo(icons()->icon("seed.png"), "Store your multisig seed to a safe location."); + + connect(ui->btn_copy, &QPushButton::clicked, this, &PageMultisigSeed::copySeed); +} + +void PageMultisigSeed::copySeed() { + Utils::copyToClipboard(m_fields->wallet->getMultisigSeed()); +} + +void PageMultisigSeed::initializePage() { + ui->seed->setPlainText(m_fields->wallet->getMultisigSeed()); +} + +int PageMultisigSeed::nextId() const { + return WalletWizard::Page_MultisigMMSRecoveryInfo; +} + +bool PageMultisigSeed::validatePage() { + return true; +} + +bool PageMultisigSeed::isComplete() const { + return true; +} diff --git a/src/wizard/multisig/PageMultisigSeed.h b/src/wizard/multisig/PageMultisigSeed.h new file mode 100644 index 0000000..51e3199 --- /dev/null +++ b/src/wizard/multisig/PageMultisigSeed.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef FEATHER_PAGEMULTISIGSEED_H +#define FEATHER_PAGEMULTISIGSEED_H + +#include +#include +#include +#include + +#include "wizard/WalletWizard.h" + +namespace Ui { + class PageMultisigSeed; +} + +class PageMultisigSeed : public QWizardPage +{ +Q_OBJECT + +public: + explicit PageMultisigSeed(WizardFields *fields, QWidget *parent = nullptr); + void initializePage() override; + bool validatePage() override; + int nextId() const override; + bool isComplete() const override; + +private: + void copySeed(); + + Ui::PageMultisigSeed *ui; + WizardFields *m_fields; +}; + + +#endif //FEATHER_PAGEMULTISIGSEED_H diff --git a/src/wizard/multisig/PageMultisigSeed.ui b/src/wizard/multisig/PageMultisigSeed.ui new file mode 100644 index 0000000..3909e52 --- /dev/null +++ b/src/wizard/multisig/PageMultisigSeed.ui @@ -0,0 +1,73 @@ + + + PageMultisigSeed + + + + 0 + 0 + 652 + 462 + + + + WizardPage + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Save to file + + + + + + + Copy + + + + + + + + + + InfoFrame + QFrame +
components.h
+ 1 +
+
+ + +
diff --git a/src/wizard/multisig/PageMultisigSetupDebug.cpp b/src/wizard/multisig/PageMultisigSetupDebug.cpp new file mode 100644 index 0000000..eb0c2db --- /dev/null +++ b/src/wizard/multisig/PageMultisigSetupDebug.cpp @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "PageMultisigSetupDebug.h" +#include "ui_PageMultisigSetupDebug.h" + +#include + +#include "utils/Icons.h" +#include "utils/Utils.h" + +#include "ringct/rctOps.h" +#include "string_tools.h" +#include "libwalletqt/MultisigMessageStore.h" + +#include "MMSWidget.h" + +PageMultisigSetupDebug::PageMultisigSetupDebug(WizardFields *fields, QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::PageMultisigSetupDebug) + , m_fields(fields) +{ + ui->setupUi(this); + this->setTitle("Creating multisig wallet"); + this->setStatus("Preparing multisig"); +// this->setCommitPage(true); +} + +void PageMultisigSetupDebug::initializePage() { +// m_mmsWidget = new MMSWidget(m_fields->wallet, this); +// ui->verticalLayout->addWidget(m_mmsWidget); +// m_mmsWidget->setModel(m_fields->wallet->mmsModel(), m_fields->wallet->mmsStore()); + m_fields->wallet->setMMSRefreshEnabled(true); + connect(m_fields->wallet->mmsStore(), &MultisigMessageStore::multisigWalletCreated, this, &PageMultisigSetupDebug::onMultisigWalletCreated); + connect(m_fields->wallet->mmsStore(), &MultisigMessageStore::statusChanged, [this](const QString &status){ + this->setStatus(status); + }); + connect(ui->btn_copyAddress, &QPushButton::clicked, [this]{ + Utils::copyToClipboard(m_fields->wallet->address(0, 0)); + }); + ui->frame_walletCreated->hide(); +} + +int PageMultisigSetupDebug::nextId() const { + return WalletWizard::Page_MultisigSeed; +} + +bool PageMultisigSetupDebug::validatePage() { + return true; +} + +bool PageMultisigSetupDebug::isComplete() const { + return true; +} + +void PageMultisigSetupDebug::onMultisigWalletCreated(const QString &address) { + this->setStatus(QString("Multisig wallet has been successfully created.")); + ui->frame_walletCreated->show(); + ui->label_address->setText(Utils::chunkAddress(address)); +} + +void PageMultisigSetupDebug::setStatus(const QString &status) { + ui->label_status->setText(QString("Status: %1").arg(status)); +} \ No newline at end of file diff --git a/src/wizard/multisig/PageMultisigSetupDebug.h b/src/wizard/multisig/PageMultisigSetupDebug.h new file mode 100644 index 0000000..458cfdf --- /dev/null +++ b/src/wizard/multisig/PageMultisigSetupDebug.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef FEATHER_PAGEMULTISIGSETUPDEBUG_H +#define FEATHER_PAGEMULTISIGSETUPDEBUG_H + +#include +#include +#include +#include + +#include "wizard/WalletWizard.h" +#include "MMSWidget.h" + +namespace Ui { + class PageMultisigSetupDebug; +} + +class PageMultisigSetupDebug : public QWizardPage +{ +Q_OBJECT + +public: + explicit PageMultisigSetupDebug(WizardFields *fields, QWidget *parent = nullptr); + void initializePage() override; + bool validatePage() override; + int nextId() const override; + bool isComplete() const override; + +signals: + void showWallet(Wallet *wallet); + +private slots: + void onMultisigWalletCreated(const QString &address); + +private: + void setStatus(const QString &status); + + Ui::PageMultisigSetupDebug *ui; + WizardFields *m_fields; + MMSWidget *m_mmsWidget; +}; + + +#endif //FEATHER_PAGEMULTISIGSETUPDEBUG_H diff --git a/src/wizard/multisig/PageMultisigSetupDebug.ui b/src/wizard/multisig/PageMultisigSetupDebug.ui new file mode 100644 index 0000000..3e7dfd7 --- /dev/null +++ b/src/wizard/multisig/PageMultisigSetupDebug.ui @@ -0,0 +1,116 @@ + + + PageMultisigSetupDebug + + + + 0 + 0 + 628 + 361 + + + + WizardPage + + + + + + Status: + + + true + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Verify the address below: + + + + + + + <html><head/><body><p><span style=" font-size:18pt;">Address</span></p></body></html> + + + true + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Copy address + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + I have verified the address + + + + + + + + diff --git a/src/wizard/multisig/PageMultisigSetupKey.cpp b/src/wizard/multisig/PageMultisigSetupKey.cpp new file mode 100644 index 0000000..364cd6d --- /dev/null +++ b/src/wizard/multisig/PageMultisigSetupKey.cpp @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "PageMultisigSetupKey.h" +#include "ui_PageMultisigSetupKey.h" + +#include "MultisigMessageStore.h" + +#include "utils/Icons.h" +#include "utils/Utils.h" + +PageMultisigSetupKey::PageMultisigSetupKey(WizardFields *fields, QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::PageMultisigSetupKey) + , m_fields(fields) +{ + ui->setupUi(this); + this->setTitle("Setup key"); + + ui->infoFrame->setInfo(icons()->icon("key.png"), "Share the setup key with other cosigners over a secure channel " + "(e.g. end-to-end encrypted group chat.)\n\nDo not share the setup " + "key with people that aren't intended cosigners for this wallet."); + + // Break at the end of the textbox + ui->line_setupKey->setWordWrapMode(QTextOption::WrapAnywhere); + + connect(ui->btn_copy, &QPushButton::clicked, [this] { + Utils::copyToClipboard(ui->line_setupKey->toPlainText()); + }); +} + +void PageMultisigSetupKey::initializePage() { + ui->line_setupKey->clear(); + + // We currently only support automatic and manual mode. + MultisigMessageStore::SetupMode mode = m_fields->multisigAutomaticSetup ? MultisigMessageStore::AUTOMATIC : MultisigMessageStore::MANUAL; + + m_setupKey = m_fields->wallet->mmsStore()->createSetupKey(m_fields->multisigThreshold, m_fields->multisigSigners, m_fields->multisigService, m_fields->multisigChannel, mode); + if (m_setupKey.isEmpty()) { + Utils::showError(this, "Unable to create setup key", "Unknown error"); + return; + } + + ui->line_setupKey->setPlainText(m_setupKey); + m_keyGenerated = true; +} + +int PageMultisigSetupKey::nextId() const { + return WalletWizard::Page_MultisigEnterName; +} + +bool PageMultisigSetupKey::validatePage() { + if (!m_keyGenerated) { + Utils::showError(this, "Unable to proceed", "No setup key was generated", {"You have found a bug. Please contact the developers."}, "report_an_issue"); + return false; + } + + m_fields->multisigSetupKey = m_setupKey; + return true; +} diff --git a/src/wizard/multisig/PageMultisigSetupKey.h b/src/wizard/multisig/PageMultisigSetupKey.h new file mode 100644 index 0000000..ee0e574 --- /dev/null +++ b/src/wizard/multisig/PageMultisigSetupKey.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef PAGEMULTISIGSETUPKEY_H +#define PAGEMULTISIGSETUPKEY_H + +#include + +#include "wizard/WalletWizard.h" + +namespace Ui { + class PageMultisigSetupKey; +} + +class PageMultisigSetupKey : public QWizardPage +{ + Q_OBJECT + + public: + explicit PageMultisigSetupKey(WizardFields *fields, QWidget *parent = nullptr); + void initializePage() override; + bool validatePage() override; + int nextId() const override; + + Ui::PageMultisigSetupKey *ui; + WizardFields *m_fields; + + QString m_setupKey; + bool m_keyGenerated = false; +}; + + +#endif //PAGEMULTISIGSETUPKEY_H diff --git a/src/wizard/multisig/PageMultisigSetupKey.ui b/src/wizard/multisig/PageMultisigSetupKey.ui new file mode 100644 index 0000000..fc7f8a2 --- /dev/null +++ b/src/wizard/multisig/PageMultisigSetupKey.ui @@ -0,0 +1,115 @@ + + + PageMultisigSetupKey + + + + 0 + 0 + 641 + 297 + + + + WizardPage + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 10 + + + + + + + + Setup key: + + + + + + + true + + + + 0 + 0 + + + + true + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Copy + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + InfoFrame + QFrame +
components.h
+ 1 +
+
+ + +
diff --git a/src/wizard/multisig/PageMultisigSetupWallet.cpp b/src/wizard/multisig/PageMultisigSetupWallet.cpp new file mode 100644 index 0000000..1e71d1b --- /dev/null +++ b/src/wizard/multisig/PageMultisigSetupWallet.cpp @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "PageMultisigSetupWallet.h" +#include "ui_PageMultisigSetupWallet.h" + +#include "libwalletqt/MultisigMessageStore.h" +#include "utils/Icons.h" +#include "utils/Utils.h" + +PageMultisigSetupWallet::PageMultisigSetupWallet(WizardFields *fields, QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::PageMultisigSetupWallet) + , m_fields(fields) +{ + ui->setupUi(this); + this->setTitle("Setting up multisig wallet"); + + ui->infoFrame->setInfo(icons()->icon("status_waiting.svg"), "Wait for the multisig wallet setup to complete."); + + ui->tree_cosigners->header()->setSectionResizeMode(0, QHeaderView::Stretch); + ui->tree_cosigners->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + ui->tree_cosigners->header()->setMinimumSectionSize(100); + + // Don't allow returning to this page. + this->setCommitPage(true); +} + +void PageMultisigSetupWallet::initializePage() { + this->setStatus("Connecting to messaging service"); + + // We have reached the next stage of the setup, + // set a cache attribute so that when we exit now we return to this page. + m_fields->wallet->setCacheAttribute("feather.multisig_setup", "configured"); + + // Since we can't go back, add a button here to copy the setup key in case the initiator forgot. + if (!m_fields->multisigInitiator) { + ui->btn_copySetupKey->hide(); + } + + // Don't show the signer configuration table until we have something to show + ui->frame_signerConfiguration->hide(); + + // Use a fast refresh interval to speed up wallet setup + m_fields->wallet->setRefreshInterval(2); + + // TODO: move elsewhere? + m_fields->wallet->mmsStore()->sendReadyMessages(); + + // Begin checking for new messages and processing them in the refresh thread + m_fields->wallet->setMMSRefreshEnabled(true); + + // Usually it's not a good idea to connect in initializePage, because re-entering from the previous page would + // duplicate the connection. However, we can't go back on this page, so it doesn't matter. + connect(m_fields->wallet->mmsStore(), &MultisigMessageStore::statusChanged, this, &PageMultisigSetupWallet::setStatus); + + // Use Qt::QueuedConnection to make sure updateSignerConfig is executed in the GUI thread, and not the refresh thread. + connect(m_fields->wallet->mmsStore(), &MultisigMessageStore::signersUpdated, this, &PageMultisigSetupWallet::updateSignerConfig, Qt::QueuedConnection); + + connect(m_fields->wallet->mmsStore(), &MultisigMessageStore::multisigWalletCreated, this, &PageMultisigSetupWallet::onWalletCreated); +} + +void PageMultisigSetupWallet::setStatus(const QString &status, bool finished) { + ui->label_status->setText(QString("Status: %1").arg(status)); + + if (finished) { + ui->infoFrame->setInfo(icons()->icon("arrow.svg"), "Proceed to the next step"); + m_fields->wallet->setRefreshInterval(10); + } +} + +void PageMultisigSetupWallet::updateSignerConfig() { + qDebug() << "updateSignerConfig"; + ui->tree_cosigners->clear(); + + // Get all signer info + auto signerInfo = m_fields->wallet->mmsStore()->getSignerInfo(); + if (signerInfo.isEmpty()) { + qDebug() << "Signer info was empty"; + return; + } + + ui->frame_signerConfiguration->show(); + + if (signerInfo.size() < m_fields->multisigSigners) { + this->setStatus(QString("Waiting for signer info (%1/%2)").arg(QString::number(signerInfo.size()), QString::number(m_fields->multisigSigners))); + } + + // Sort signers by label + std::sort(signerInfo.begin(), signerInfo.end(), [](const MultisigMessageStore::SignerInfo &a, const MultisigMessageStore::SignerInfo &b){ + return a.label < b.label; + }); + + for (const auto &info : signerInfo) { + auto *sItem = new QTreeWidgetItem(ui->tree_cosigners); + sItem->setText(0, info.label); + sItem->setText(1, info.publicKey); + sItem->setFont(1, Utils::getMonospaceFont()); + } +} + +void PageMultisigSetupWallet::onWalletCreated() { + m_fields->wallet->setCacheAttribute("feather.multisig_setup", "verify"); + m_created = true; + completeChanged(); +} + +int PageMultisigSetupWallet::nextId() const { + return WalletWizard::Page_MultisigVerifyAddress; +} + +bool PageMultisigSetupWallet::validatePage() { + return true; +} + +bool PageMultisigSetupWallet::isComplete() const { + if (!m_created) { + return false; + } + + return true; +} diff --git a/src/wizard/multisig/PageMultisigSetupWallet.h b/src/wizard/multisig/PageMultisigSetupWallet.h new file mode 100644 index 0000000..8ad7b9b --- /dev/null +++ b/src/wizard/multisig/PageMultisigSetupWallet.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef FEATHER_PAGEMULTISIGSETUPWALLET_H +#define FEATHER_PAGEMULTISIGSETUPWALLET_H + +#include + +#include "wizard/WalletWizard.h" + +namespace Ui { + class PageMultisigSetupWallet; +} + +class PageMultisigSetupWallet : public QWizardPage +{ +Q_OBJECT + +public: + explicit PageMultisigSetupWallet(WizardFields *fields, QWidget *parent = nullptr); + void initializePage() override; + bool validatePage() override; + int nextId() const override; + bool isComplete() const override; + +private slots: + void setStatus(const QString &status, bool finished = false); + void updateSignerConfig(); + void onWalletCreated(); + +private: + Ui::PageMultisigSetupWallet *ui; + WizardFields *m_fields; + bool m_created = false; +}; + + +#endif //FEATHER_PAGEMULTISIGSETUPWALLET_H diff --git a/src/wizard/multisig/PageMultisigSetupWallet.ui b/src/wizard/multisig/PageMultisigSetupWallet.ui new file mode 100644 index 0000000..b4b5b10 --- /dev/null +++ b/src/wizard/multisig/PageMultisigSetupWallet.ui @@ -0,0 +1,156 @@ + + + PageMultisigSetupWallet + + + + 0 + 0 + 708 + 454 + + + + WizardPage + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + Status: Connecting to message service + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + + + QAbstractItemView::NoSelection + + + false + + + false + + + + Name + + + + + Public key + + + + + + + + + + + Qt::Vertical + + + + 20 + 5 + + + + + + + + + + Copy setup key + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + InfoFrame + QFrame +
components.h
+ 1 +
+
+ + +
diff --git a/src/wizard/multisig/PageMultisigSignerConfig.cpp b/src/wizard/multisig/PageMultisigSignerConfig.cpp new file mode 100644 index 0000000..f950267 --- /dev/null +++ b/src/wizard/multisig/PageMultisigSignerConfig.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "PageMultisigSignerConfig.h" +#include "ui_PageMultisigSignerConfig.h" + +#include "utils/Icons.h" + +PageMultisigSignerConfig::PageMultisigSignerConfig(WizardFields *fields, QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::PageMultisigSignerConfig) + , m_fields(fields) +{ + ui->setupUi(this); + this->setTitle("Create setup key (3/3)"); + + ui->infoFrame->setInfo(icons()->icon("sign.png"), "Choose the signer configuration mode."); +} + +void PageMultisigSignerConfig::initializePage() { + ui->radio_automatic->setChecked(true); +} + +int PageMultisigSignerConfig::nextId() const { + return WalletWizard::Page_MultisigShowSetupKey; +} + +bool PageMultisigSignerConfig::validatePage() { + m_fields->multisigAutomaticSetup = ui->radio_automatic->isChecked(); + return true; +} + diff --git a/src/wizard/multisig/PageMultisigSignerConfig.h b/src/wizard/multisig/PageMultisigSignerConfig.h new file mode 100644 index 0000000..366108f --- /dev/null +++ b/src/wizard/multisig/PageMultisigSignerConfig.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef PAGEMULTISIGSIGNERCONFIG_H +#define PAGEMULTISIGSIGNERCONFIG_H + +#include + +#include "wizard/WalletWizard.h" + +namespace Ui { + class PageMultisigSignerConfig; +} + +class PageMultisigSignerConfig : public QWizardPage +{ + Q_OBJECT + + public: + explicit PageMultisigSignerConfig(WizardFields *fields, QWidget *parent = nullptr); + void initializePage() override; + bool validatePage() override; + int nextId() const override; + + Ui::PageMultisigSignerConfig *ui; + WizardFields *m_fields; +}; + +#endif //PAGEMULTISIGSIGNERCONFIG_H diff --git a/src/wizard/multisig/PageMultisigSignerConfig.ui b/src/wizard/multisig/PageMultisigSignerConfig.ui new file mode 100644 index 0000000..eac5ba8 --- /dev/null +++ b/src/wizard/multisig/PageMultisigSignerConfig.ui @@ -0,0 +1,142 @@ + + + PageMultisigSignerConfig + + + + 0 + 0 + 433 + 321 + + + + WizardPage + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 18 + 18 + + + + + + + + Automatic signer configuration + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 40 + 20 + + + + + + + + May cause problems during setup if a cosigner is dishonest. + + + true + + + + + + + + + Manual signer configuration + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 40 + 20 + + + + + + + + All participants must manually enter eachother's signer info. + + + true + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + InfoFrame + QFrame +
components.h
+ 1 +
+
+ + +
diff --git a/src/wizard/multisig/PageMultisigSignerInfo.cpp b/src/wizard/multisig/PageMultisigSignerInfo.cpp new file mode 100644 index 0000000..97e8b7e --- /dev/null +++ b/src/wizard/multisig/PageMultisigSignerInfo.cpp @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "PageMultisigSignerInfo.h" +#include "ui_PageMultisigSignerInfo.h" + +#include + +#include "utils/Icons.h" +#include "utils/Utils.h" + +#include "ringct/rctOps.h" +#include "string_tools.h" +#include "libwalletqt/MultisigMessageStore.h" + +PageMultisigSignerInfo::PageMultisigSignerInfo(WizardFields *fields, QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::PageMultisigSignerInfo) + , m_fields(fields) +{ + ui->setupUi(this); + this->setTitle("Enter signer info"); +// this->setCommitPage(true); + + ui->infoFrame->setInfo(icons()->icon("sign.png"), "Enter a label and the address for each cosigner."); +} + +void PageMultisigSignerInfo::initializePage() { + while (QLayoutItem* item = ui->signerLayout->takeAt(0)) { + delete item->widget(); + delete item; + } + m_signerInfo.clear(); + + for (int i = 1; i < m_fields->multisigSigners; i++) { + auto *name = new QLineEdit(this); + auto *address = new QLineEdit(this); + + m_signerInfo.push_back({name, address}); + ui->signerLayout->addRow(name, address); + } +} + +int PageMultisigSignerInfo::nextId() const { + return WalletWizard::Page_MultisigSetupWallet; +} + +bool PageMultisigSignerInfo::validatePage() { + // TODO: error handling + for (int i = 0; i < m_signerInfo.size(); i++) { + const auto& info = m_signerInfo[i]; + m_fields->wallet->mmsStore()->setSigner(i+1, info.first->text(), info.second->text()); + } + + m_fields->wallet->mmsStore()->next(); + + return true; +} + +bool PageMultisigSignerInfo::isComplete() const { + return true; +} + diff --git a/src/wizard/multisig/PageMultisigSignerInfo.h b/src/wizard/multisig/PageMultisigSignerInfo.h new file mode 100644 index 0000000..e273ed2 --- /dev/null +++ b/src/wizard/multisig/PageMultisigSignerInfo.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef FEATHER_PAGEMULTISIGSIGNERINFO_H +#define FEATHER_PAGEMULTISIGSIGNERINFO_H + +#include +#include +#include +#include +#include + +#include "wizard/WalletWizard.h" + +namespace Ui { + class PageMultisigSignerInfo; +} + +class PageMultisigSignerInfo : public QWizardPage +{ +Q_OBJECT + +public: + explicit PageMultisigSignerInfo(WizardFields *fields, QWidget *parent = nullptr); + void initializePage() override; + bool validatePage() override; + int nextId() const override; + bool isComplete() const override; + +private slots: + +private: + Ui::PageMultisigSignerInfo *ui; + WizardFields *m_fields; + + QVector> m_signerInfo; +}; + + +#endif //FEATHER_PAGEMULTISIGSIGNERINFO_H diff --git a/src/wizard/multisig/PageMultisigSignerInfo.ui b/src/wizard/multisig/PageMultisigSignerInfo.ui new file mode 100644 index 0000000..ad8dde2 --- /dev/null +++ b/src/wizard/multisig/PageMultisigSignerInfo.ui @@ -0,0 +1,76 @@ + + + PageMultisigSignerInfo + + + + 0 + 0 + 708 + 328 + + + + WizardPage + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + true + + + + + 0 + 0 + 688 + 292 + + + + + + + + + Label + + + + + + + Address + + + + + + + + + + + + + + InfoFrame + QFrame +
components.h
+ 1 +
+
+ + +
diff --git a/src/wizard/multisig/PageMultisigVerifyAddress.cpp b/src/wizard/multisig/PageMultisigVerifyAddress.cpp new file mode 100644 index 0000000..87e540a --- /dev/null +++ b/src/wizard/multisig/PageMultisigVerifyAddress.cpp @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#include "PageMultisigVerifyAddress.h" +#include "ui_PageMultisigVerifyAddress.h" + +#include + +#include "utils/Icons.h" +#include "utils/Utils.h" + +#include "ringct/rctOps.h" +#include "string_tools.h" +#include "libwalletqt/MultisigMessageStore.h" + +PageMultisigVerifyAddress::PageMultisigVerifyAddress(WizardFields *fields, QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::PageMultisigVerifyAddress) + , m_fields(fields) +{ + ui->setupUi(this); + this->setTitle("Verify multisig address"); +// this->setCommitPage(true); + + ui->infoFrame->setInfo(icons()->icon("tab_addresses.png"), "Verify that all participants share the same multisig address."); + + connect(ui->check_verify, &QCheckBox::toggled, [this](bool checked){ + completeChanged(); + }); + + connect(ui->btn_copyAddress, &QPushButton::clicked, [this] { + Utils::copyToClipboard(m_address); + }); +} + +void PageMultisigVerifyAddress::initializePage() { + bool ok; + QString reason; + m_address = m_fields->wallet->getAddressSafe(0, 0, ok, reason); + + if (!ok) { + ui->label_address->setText(""); + Utils::showError(this, "Unable to get multisig address", reason); + return; + } + + ui->label_address->setText(Utils::chunkAddress(m_address)); + ui->label_address->setFont(Utils::getMonospaceFont()); +} + +int PageMultisigVerifyAddress::nextId() const { + return WalletWizard::Page_MultisigSeed; +} + +bool PageMultisigVerifyAddress::validatePage() { + return true; +} + +bool PageMultisigVerifyAddress::isComplete() const { + if (!ui->check_verify->isChecked()) { + return false; + } + + return true; +} + diff --git a/src/wizard/multisig/PageMultisigVerifyAddress.h b/src/wizard/multisig/PageMultisigVerifyAddress.h new file mode 100644 index 0000000..14aae39 --- /dev/null +++ b/src/wizard/multisig/PageMultisigVerifyAddress.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2024 The Monero Project + +#ifndef FEATHER_PAGEMULTISIGVERIFYADDRESS_H +#define FEATHER_PAGEMULTISIGVERIFYADDRESS_H + + +#include +#include +#include +#include + +#include "wizard/WalletWizard.h" + +namespace Ui { + class PageMultisigVerifyAddress; +} + +class PageMultisigVerifyAddress : public QWizardPage +{ +Q_OBJECT + +public: + explicit PageMultisigVerifyAddress(WizardFields *fields, QWidget *parent = nullptr); + void initializePage() override; + bool validatePage() override; + int nextId() const override; + bool isComplete() const override; + +private slots: + +private: + Ui::PageMultisigVerifyAddress *ui; + WizardFields *m_fields; + QString m_address; +}; + + +#endif //FEATHER_PAGEMULTISIGVERIFYADDRESS_H diff --git a/src/wizard/multisig/PageMultisigVerifyAddress.ui b/src/wizard/multisig/PageMultisigVerifyAddress.ui new file mode 100644 index 0000000..f8ad175 --- /dev/null +++ b/src/wizard/multisig/PageMultisigVerifyAddress.ui @@ -0,0 +1,106 @@ + + + PageMultisigVerifyAddress + + + + 0 + 0 + 574 + 449 + + + + WizardPage + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + Multisig address: + + + + + + + TextLabel + + + true + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Copy address + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + 0 + 0 + + + + I have verified the address + + + + + + + + InfoFrame + QFrame +
components.h
+ 1 +
+
+ + +