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
+
+ 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
+
+ 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
+
+ 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
+
+ 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
+
+ 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
+
+ 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
+
+ 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
+
+ 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
+
+ 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
+
+ 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
+
+ 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
+
+ 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
+
+ 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
+
+ 1
+
+
+
+
+