multisig [DO NOT USE IN PRODUCTION]
Some checks failed
ci/gh-actions/build / build-ubuntu-without-scanner (push) Has been cancelled
ci/gh-actions/guix / cache-sources (push) Has been cancelled
ci/gh-actions/guix / ${{ matrix.toolchain.target }} (map[target:aarch64-linux-gnu]) (push) Has been cancelled
ci/gh-actions/guix / ${{ matrix.toolchain.target }} (map[target:arm-linux-gnueabihf]) (push) Has been cancelled
ci/gh-actions/guix / ${{ matrix.toolchain.target }} (map[target:arm64-apple-darwin]) (push) Has been cancelled
ci/gh-actions/guix / ${{ matrix.toolchain.target }} (map[target:i686-linux-gnu]) (push) Has been cancelled
ci/gh-actions/guix / ${{ matrix.toolchain.target }} (map[target:riscv64-linux-gnu]) (push) Has been cancelled
ci/gh-actions/guix / ${{ matrix.toolchain.target }} (map[target:x86_64-apple-darwin]) (push) Has been cancelled
ci/gh-actions/guix / ${{ matrix.toolchain.target }} (map[target:x86_64-linux-gnu.no-tor-bundle]) (push) Has been cancelled
ci/gh-actions/guix / ${{ matrix.toolchain.target }} (map[target:x86_64-linux-gnu.pack]) (push) Has been cancelled
ci/gh-actions/guix / ${{ matrix.toolchain.target }} (map[target:x86_64-linux-gnu]) (push) Has been cancelled
ci/gh-actions/guix / ${{ matrix.toolchain.target }} (map[target:x86_64-w64-mingw32.installer]) (push) Has been cancelled
ci/gh-actions/guix / ${{ matrix.toolchain.target }} (map[target:x86_64-w64-mingw32]) (push) Has been cancelled
ci/gh-actions/guix / bundle-logs (push) Has been cancelled

This commit is contained in:
tobtoht 2024-07-17 18:21:03 +02:00
parent 007933f2e0
commit f944b6e6f7
132 changed files with 7522 additions and 365 deletions

View file

@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.18) cmake_minimum_required(VERSION 3.18)
project(feather project(feather
VERSION "2.6.5" VERSION "2.6.7"
DESCRIPTION "A free Monero desktop wallet" DESCRIPTION "A free Monero desktop wallet"
LANGUAGES CXX C ASM LANGUAGES CXX C ASM
) )
@ -284,10 +284,13 @@ if (WIN32)
endif() endif()
if(STATIC) if(STATIC)
# add_linker_flag_if_supported(-static-libgcc STATIC_FLAGS)
# add_linker_flag_if_supported(-static-libstdc++ STATIC_FLAGS)
if(MINGW) if(MINGW)
add_linker_flag_if_supported(-static STATIC_FLAGS) 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()
endif() endif()

2
monero

@ -1 +1 @@
Subproject commit 892a9a16d169ecd8f15e892f1e14fdd84cfce0f3 Subproject commit 5458a8a6630fa4c75e9c1df9e2828739afa88124

View file

@ -20,8 +20,8 @@ endif()
find_package(Qt6 REQUIRED COMPONENTS ${QT_COMPONENTS}) find_package(Qt6 REQUIRED COMPONENTS ${QT_COMPONENTS})
set(QAPPLICATION_CLASS QApplication CACHE STRING "Inheritance class for SingleApplication") #set(QAPPLICATION_CLASS QApplication CACHE STRING "Inheritance class for SingleApplication")
add_subdirectory(third-party/singleapplication) #add_subdirectory(third-party/singleapplication)
if (CHECK_UPDATES) if (CHECK_UPDATES)
add_subdirectory(openpgp) add_subdirectory(openpgp)
@ -55,6 +55,8 @@ file(GLOB SOURCE_FILES
"widgets/*.cpp" "widgets/*.cpp"
"wizard/*.h" "wizard/*.h"
"wizard/*.cpp" "wizard/*.cpp"
"wizard/multisig/*.h"
"wizard/multisig/*.cpp"
"wallet/*.h" "wallet/*.h"
"wallet/*.cpp" "wallet/*.cpp"
"qrcode/*.h" "qrcode/*.h"
@ -272,7 +274,7 @@ target_link_libraries(feather PRIVATE
Threads::Threads Threads::Threads
${QRENCODE_LIBRARY} ${QRENCODE_LIBRARY}
${POLYSEED_LIBRARY} ${POLYSEED_LIBRARY}
SingleApplication::SingleApplication # SingleApplication::SingleApplication
${ICU_LIBRARIES} ${ICU_LIBRARIES}
${LIBZIP_LIBRARIES} ${LIBZIP_LIBRARIES}
${ZLIB_LIBRARIES} ${ZLIB_LIBRARIES}

View file

@ -89,11 +89,8 @@ void CoinsWidget::setModel(CoinsModel * model, Coins * coins) {
ui->coins->setColumnHidden(CoinsModel::SpentHeight, true); ui->coins->setColumnHidden(CoinsModel::SpentHeight, true);
ui->coins->setColumnHidden(CoinsModel::Frozen, true); ui->coins->setColumnHidden(CoinsModel::Frozen, true);
if (!m_wallet->viewOnly()) { bool showKeyImageKnown = m_wallet->viewOnly() || m_wallet->isMultisig();
ui->coins->setColumnHidden(CoinsModel::KeyImageKnown, true); ui->coins->setColumnHidden(CoinsModel::KeyImageKnown, !showKeyImageKnown);
} else {
ui->coins->setColumnHidden(CoinsModel::KeyImageKnown, false);
}
ui->coins->header()->setSectionResizeMode(QHeaderView::ResizeToContents); ui->coins->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
ui->coins->header()->setSectionResizeMode(CoinsModel::Label, QHeaderView::Stretch); ui->coins->header()->setSectionResizeMode(CoinsModel::Label, QHeaderView::Stretch);
@ -335,6 +332,11 @@ bool CoinsWidget::isCoinSpendable(CoinsInfo *coin) {
return false; 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()) { if (coin->spent()) {
Utils::showError(this, "Unable to spend outputs", "Selected output was already spent"); Utils::showError(this, "Unable to spend outputs", "Selected output was already spent");
return false; return false;

152
src/MMSWidget.cpp Normal file
View file

@ -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 <QMessageBox>
#include <QHeaderView>
#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;

44
src/MMSWidget.h Normal file
View file

@ -0,0 +1,44 @@
//
// Created by user on 1/3/24.
//
#ifndef FEATHER_MMSWIDGET_H
#define FEATHER_MMSWIDGET_H
#include <QMenu>
#include <QWidget>
#include <QSvgWidget>
#include <QTreeView>
#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::MMSWidget> ui;
Wallet *m_wallet;
MultisigMessageStore *m_store;
MultisigMessageModel * m_model;
};
#endif //FEATHER_MMSWIDGET_H

112
src/MMSWidget.ui Normal file
View file

@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MMSWidget</class>
<widget class="QWidget" name="MMSWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>857</width>
<height>464</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>Transaction proposals</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QTreeView" name="transactionProposals">
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>Message store</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTreeView" name="mms"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="btn_mmsNext">
<property name="text">
<string>Next</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_mmsNextSync">
<property name="text">
<string>Next (force sync)</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_exportMultisig">
<property name="text">
<string>Export multisig</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_deleteAll">
<property name="text">
<string>Delete all</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -239,6 +239,10 @@ void MainWindow::initWidgets() {
m_coinsWidget = new CoinsWidget(m_wallet, this); m_coinsWidget = new CoinsWidget(m_wallet, this);
ui->coinsWidgetLayout->addWidget(m_coinsWidget); ui->coinsWidgetLayout->addWidget(m_coinsWidget);
// [MMS]
m_mmsWidget = new MMSWidget(m_wallet, this);
ui->mmsWidgetLayout->addWidget(m_mmsWidget);
// [Plugins..] // [Plugins..]
for (auto* plugin : m_plugins) { for (auto* plugin : m_plugins) {
if (!plugin->hasParent()) { if (!plugin->hasParent()) {
@ -485,6 +489,7 @@ void MainWindow::initWalletContext() {
}); });
connect(m_wallet, &Wallet::multiBroadcast, this, &MainWindow::onMultiBroadcast); connect(m_wallet, &Wallet::multiBroadcast, this, &MainWindow::onMultiBroadcast);
connect(m_wallet->mmsStore(), &MultisigMessageStore::askToSign, this, &MainWindow::onAskToSign);
} }
void MainWindow::menuToggleTabVisible(const QString &key){ void MainWindow::menuToggleTabVisible(const QString &key){
@ -523,6 +528,8 @@ QString MainWindow::walletKeysPath() {
} }
void MainWindow::onWalletOpened() { void MainWindow::onWalletOpened() {
qDebug() << this->thread();
qDebug() << Q_FUNC_INFO; qDebug() << Q_FUNC_INFO;
m_splashDialog->hide(); m_splashDialog->hide();
@ -553,6 +560,13 @@ void MainWindow::onWalletOpened() {
m_coinsWidget->setModel(m_wallet->coinsModel(), m_wallet->coins()); m_coinsWidget->setModel(m_wallet->coinsModel(), m_wallet->coins());
m_wallet->coinsModel()->setCurrentSubaddressAccount(m_wallet->currentSubaddressAccount()); 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 // Coin labeling uses set_tx_note, so we need to refresh history too
connect(m_wallet->coins(), &Coins::descriptionChanged, [this] { connect(m_wallet->coins(), &Coins::descriptionChanged, [this] {
m_wallet->history()->refresh(); m_wallet->history()->refresh();
@ -695,6 +709,47 @@ void MainWindow::onMultiBroadcast(const QMap<QString, QString> &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) { void MainWindow::onSyncStatus(quint64 height, quint64 target, bool daemonSync) {
if (height >= (target - 1)) { if (height >= (target - 1)) {
this->updateNetStats(); this->updateNetStats();
@ -957,8 +1012,17 @@ void MainWindow::onTransactionCreated(PendingTransaction *tx, const QVector<QStr
break; break;
} }
case QDialog::Accepted: case QDialog::Accepted:
m_wallet->commitTransaction(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; break;
}
} }
if (dialog.showAdvanced) { if (dialog.showAdvanced) {
@ -1037,7 +1101,7 @@ void MainWindow::showSeedDialog() {
return; 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", Utils::showInfo(this, "Seed unavailable", "Wallet is non-deterministic and has no seed",
{"To obtain wallet keys go to Wallet -> Keys"}, "show_wallet_seed"); {"To obtain wallet keys go to Wallet -> Keys"}, "show_wallet_seed");
return; return;
@ -1047,6 +1111,12 @@ void MainWindow::showSeedDialog() {
return; 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}; SeedDialog dialog{m_wallet, this};
dialog.exec(); dialog.exec();
} }
@ -1682,6 +1752,10 @@ void MainWindow::updateTitle() {
title += " [view-only]"; title += " [view-only]";
} }
if (m_wallet->isMultisig()) {
title += " [multisig]";
}
title += " - Feather"; title += " - Feather";
this->setWindowTitle(title); this->setWindowTitle(title);

View file

@ -38,6 +38,7 @@
#include "SendWidget.h" #include "SendWidget.h"
#include "ReceiveWidget.h" #include "ReceiveWidget.h"
#include "CoinsWidget.h" #include "CoinsWidget.h"
#include "MMSWidget.h"
#include "WindowManager.h" #include "WindowManager.h"
#include "plugins/Plugin.h" #include "plugins/Plugin.h"
@ -165,6 +166,7 @@ private slots:
void onProxySettingsChanged(); void onProxySettingsChanged();
void onOfflineMode(bool offline); void onOfflineMode(bool offline);
void onMultiBroadcast(const QMap<QString, QString> &txHexMap); void onMultiBroadcast(const QMap<QString, QString> &txHexMap);
void onAskToSign(PendingTransaction *tx);
private: private:
friend WindowManager; friend WindowManager;
@ -220,6 +222,7 @@ private:
SendWidget *m_sendWidget = nullptr; SendWidget *m_sendWidget = nullptr;
ReceiveWidget *m_receiveWidget = nullptr; ReceiveWidget *m_receiveWidget = nullptr;
CoinsWidget *m_coinsWidget = nullptr; CoinsWidget *m_coinsWidget = nullptr;
MMSWidget *m_mmsWidget = nullptr;
QPointer<QAction> m_clearRecentlyOpenAction; QPointer<QAction> m_clearRecentlyOpenAction;

View file

@ -59,7 +59,7 @@
<item> <item>
<widget class="QTabWidget" name="tabWidget"> <widget class="QTabWidget" name="tabWidget">
<property name="currentIndex"> <property name="currentIndex">
<number>0</number> <number>4</number>
</property> </property>
<property name="iconSize"> <property name="iconSize">
<size> <size>
@ -158,6 +158,20 @@
</item> </item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="tab">
<attribute name="icon">
<iconset resource="assets.qrc">
<normaloff>:/assets/images/sign.png</normaloff>:/assets/images/sign.png</iconset>
</attribute>
<attribute name="title">
<string>Multisig</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QVBoxLayout" name="mmsWidgetLayout"/>
</item>
</layout>
</widget>
</widget> </widget>
</item> </item>
<item> <item>

View file

@ -243,6 +243,9 @@ void SendWidget::sendClicked() {
#endif #endif
} }
// TODO: force key image sync here for multisig wallets?
// if (m_wallet->isMultisig() && m_wallet->mul)
m_wallet->createTransaction(recipient, amount, description, sendAll); m_wallet->createTransaction(recipient, amount, description, sendAll);
} }

View file

@ -26,7 +26,7 @@ WindowManager::WindowManager(QObject *parent)
m_splashDialog = new SplashDialog(); m_splashDialog = new SplashDialog();
m_cleanupThread = new QThread(this); 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::walletCreated, this, &WindowManager::onWalletCreated);
connect(m_walletManager, &WalletManager::deviceButtonRequest, this, &WindowManager::onDeviceButtonRequest); connect(m_walletManager, &WalletManager::deviceButtonRequest, this, &WindowManager::onDeviceButtonRequest);
connect(m_walletManager, &WalletManager::deviceButtonPressed, this, &WindowManager::onDeviceButtonPressed); connect(m_walletManager, &WalletManager::deviceButtonPressed, this, &WindowManager::onDeviceButtonPressed);
@ -75,6 +75,16 @@ void WindowManager::quitAfterLastWindow() {
return; 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."; qDebug() << "No wizards in progress and no wallets open, quitting application.";
this->close(); this->close();
} }
@ -85,10 +95,18 @@ void WindowManager::close() {
window->close(); window->close();
} }
m_wizard->deleteLater(); if (m_splashDialog) {
m_splashDialog->deleteLater(); m_splashDialog->deleteLater();
m_tray->deleteLater(); }
m_docsDialog->deleteLater(); if (m_tray) {
m_tray->deleteLater();
}
if (m_wizard) {
m_wizard->deleteLater();
}
if (m_docsDialog) {
m_docsDialog->deleteLater();
}
torManager()->stop(); torManager()->stop();
@ -296,10 +314,28 @@ void WindowManager::onWalletOpened(Wallet *wallet) {
m_splashDialog->hide(); m_splashDialog->hide();
m_openWalletTriedOnce = false; 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); auto *window = new MainWindow(this, wallet);
m_windows.append(window); m_windows.append(window);
this->buildTrayMenu();
m_openingWallet = false; m_openingWallet = false;
this->buildTrayMenu();
} }
void WindowManager::onWalletOpenPasswordRequired(bool invalidPassword, const QString &path) { void WindowManager::onWalletOpenPasswordRequired(bool invalidPassword, const QString &path) {
@ -338,7 +374,7 @@ bool WindowManager::autoOpenWallet() {
// ######################## WALLET CREATION ######################## // ######################## WALLET CREATION ########################
void WindowManager::tryCreateWallet(Seed seed, const QString &path, const QString &password, const QString &seedLanguage, 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)) { if (Utils::fileExists(path)) {
this->handleWalletError({nullptr, Utils::ERROR, "Failed to create wallet", QString("File already exists: %1").arg(path)}); this->handleWalletError({nullptr, Utils::ERROR, "Failed to create wallet", QString("File already exists: %1").arg(path)});
return; return;
@ -366,7 +402,14 @@ void WindowManager::tryCreateWallet(Seed seed, const QString &path, const QStrin
wallet->setCacheAttribute("feather.seedoffset", seedOffset); wallet->setCacheAttribute("feather.seedoffset", seedOffset);
if (newWallet) { 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); this->onWalletOpened(wallet);
@ -417,6 +460,26 @@ void WindowManager::tryCreateWalletFromKeys(const QString &path, const QString &
this->onWalletOpened(wallet); 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) { void WindowManager::onWalletCreated(Wallet *wallet) {
// Currently only called when a wallet is created from device. // Currently only called when a wallet is created from device.
auto state = wallet->status(); auto state = wallet->status();
@ -686,6 +749,8 @@ WalletWizard* WindowManager::createWizard(WalletWizard::Page startPage) {
connect(wizard, &WalletWizard::createWallet, this, &WindowManager::tryCreateWallet); connect(wizard, &WalletWizard::createWallet, this, &WindowManager::tryCreateWallet);
connect(wizard, &WalletWizard::createWalletFromKeys, this, &WindowManager::tryCreateWalletFromKeys); connect(wizard, &WalletWizard::createWalletFromKeys, this, &WindowManager::tryCreateWalletFromKeys);
connect(wizard, &WalletWizard::createWalletFromDevice, this, &WindowManager::tryCreateWalletFromDevice); 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; return wizard;
} }
@ -698,12 +763,17 @@ void WindowManager::initWizard() {
this->showWizard(startPage); this->showWizard(startPage);
} }
void WindowManager::showWizard(WalletWizard::Page startPage) { void WindowManager::showWizard(WalletWizard::Page startPage, Wallet *wallet) {
if (!m_wizard) { if (!m_wizard) {
m_wizard = this->createWizard(startPage); m_wizard = this->createWizard(startPage);
} }
m_wizard->resetFields(); m_wizard->resetFields();
if (wallet) {
m_wizard->setWallet(wallet);
}
m_wizard->setStartId(startPage); m_wizard->setStartId(startPage);
m_wizard->restart(); m_wizard->restart();
m_wizard->setEnabled(true); m_wizard->setEnabled(true);

View file

@ -28,7 +28,7 @@ public:
void wizardOpenWallet(); void wizardOpenWallet();
void close(); void close();
void closeWindow(MainWindow *window); void closeWindow(MainWindow *window);
void showWizard(WalletWizard::Page startPage); void showWizard(WalletWizard::Page startPage, Wallet *wallet = nullptr);
void restartApplication(const QString &binaryFilename); void restartApplication(const QString &binaryFilename);
void raise(); void raise();
@ -68,9 +68,10 @@ private slots:
void onChangeTheme(const QString &themeName); void onChangeTheme(const QString &themeName);
private: 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 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 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(); bool autoOpenWallet();
@ -107,6 +108,7 @@ private:
bool m_openWalletTriedOnce = false; bool m_openWalletTriedOnce = false;
bool m_openingWallet = false; bool m_openingWallet = false;
bool m_initialNetworkConfigured = false; bool m_initialNetworkConfigured = false;
bool m_aboutToShowWizard = false;
QThread *m_cleanupThread; QThread *m_cleanupThread;
}; };

View file

@ -39,6 +39,7 @@
<file>assets/images/eye_blind.png</file> <file>assets/images/eye_blind.png</file>
<file>assets/images/file.png</file> <file>assets/images/file.png</file>
<file>assets/images/file_manager_32px.png</file> <file>assets/images/file_manager_32px.png</file>
<file>assets/images/freeze.png</file>
<file>assets/images/gnome-calc.png</file> <file>assets/images/gnome-calc.png</file>
<file>assets/images/hd_32px.png</file> <file>assets/images/hd_32px.png</file>
<file>assets/images/history.png</file> <file>assets/images/history.png</file>
@ -58,6 +59,7 @@
<file>assets/images/localMonero_logo.png</file> <file>assets/images/localMonero_logo.png</file>
<file>assets/images/localMonero_register.svg</file> <file>assets/images/localMonero_register.svg</file>
<file>assets/images/lock.svg</file> <file>assets/images/lock.svg</file>
<file>assets/images/mail.png</file>
<file>assets/images/microphone.png</file> <file>assets/images/microphone.png</file>
<file>assets/images/mining.png</file> <file>assets/images/mining.png</file>
<file>assets/images/network.png</file> <file>assets/images/network.png</file>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
src/assets/images/mail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -50,7 +50,7 @@ AccountSwitcherDialog::AccountSwitcherDialog(Wallet *wallet, QWidget *parent)
connect(m_wallet->subaddressAccount(), &SubaddressAccount::refreshFinished, this, &AccountSwitcherDialog::updateSelection); connect(m_wallet->subaddressAccount(), &SubaddressAccount::refreshFinished, this, &AccountSwitcherDialog::updateSelection);
this->update(); this->update();
this->updateSelection(); // this->updateSelection();
} }
void AccountSwitcherDialog::update() { void AccountSwitcherDialog::update() {

View file

@ -21,28 +21,35 @@ SeedDialog::SeedDialog(Wallet *wallet, QWidget *parent)
m_wallet->setSeedLanguage(constants::seedLanguage); m_wallet->setSeedLanguage(constants::seedLanguage);
} }
QString seedOffset = m_wallet->getCacheAttribute("feather.seedoffset"); if (m_wallet->isMultisig()) {
QString seed = m_wallet->getCacheAttribute("feather.seed"); this->setMultisigSeed(m_wallet->getMultisigSeed());
auto seedLength = m_wallet->seedLength(); ui->frameSeedOffset->setVisible(false);
QString seed_25_words = m_wallet->getSeed(seedOffset);
if (seedLength >= 24) {
ui->check_toggleSeedType->hide(); ui->check_toggleSeedType->hide();
this->setSeed(seed_25_words);
} else {
this->setSeed(seed);
ui->frameRestoreHeight->setVisible(false); 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, " ui->label_restoreHeightHelp->setHelpText("", "Should you restore your wallet in the future, "
"specifying this block number will recover your wallet quicker.", "restore_height"); "specifying this block number will recover your wallet quicker.", "restore_height");
@ -59,10 +66,23 @@ void SeedDialog::setSeed(const QString &seed) {
"</p>" "</p>"
"<b>WARNING:</b>" "<b>WARNING:</b>"
"<ul>" "<ul>"
"<li>Never disclose your seed.</li>" "<li>Never disclose your seed</li>"
"<li>Never type it on a website</li>" "<li>Never type it on a website</li>"
"<li>Do not store it electronically</li>" "<li>Do not store it electronically</li>"
"</ul>").arg(words)); "</ul>").arg(words));
} }
void SeedDialog::setMultisigSeed(const QString &seed) {
ui->seed->setPlainText(seed);
ui->label_warning->setText("<p>This seed will allow you to recover your wallet in case "
"of computer failure."
"</p>"
"<b>WARNING:</b>"
"<ul>"
"<li>Never disclose your seed</li>"
"<li>Never type it on a website</li>"
"</ul>");
}
SeedDialog::~SeedDialog() = default; SeedDialog::~SeedDialog() = default;

View file

@ -23,6 +23,7 @@ public:
private: private:
void setSeed(const QString &seed); void setSeed(const QString &seed);
void setMultisigSeed(const QString &seed);
QScopedPointer<Ui::SeedDialog> ui; QScopedPointer<Ui::SeedDialog> ui;
Wallet *m_wallet; Wallet *m_wallet;

View file

@ -103,6 +103,38 @@ void TxConfAdvDialog::setUnsignedTransaction(UnsignedTransaction *utx) {
this->setupConstructionData(ci); 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) { void TxConfAdvDialog::setAmounts(quint64 amount, quint64 fee) {
QString preferredCur = conf()->get(Config::preferredFiatCurrency).toString(); QString preferredCur = conf()->get(Config::preferredFiatCurrency).toString();
@ -180,6 +212,29 @@ void TxConfAdvDialog::setupConstructionData(ConstructionInfo *ci) {
} }
void TxConfAdvDialog::signTransaction() { 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(); this->accept();
} }

View file

@ -28,6 +28,7 @@ public:
void setTransaction(PendingTransaction *tx, bool isSigned = true); // #TODO: have libwallet return a UnsignedTransaction, this is just dumb void setTransaction(PendingTransaction *tx, bool isSigned = true); // #TODO: have libwallet return a UnsignedTransaction, this is just dumb
void setUnsignedTransaction(UnsignedTransaction *utx); void setUnsignedTransaction(UnsignedTransaction *utx);
void setMultisigTransaction(PendingTransaction *tx);
private: private:
void setupConstructionData(ConstructionInfo *ci); void setupConstructionData(ConstructionInfo *ci);
@ -51,6 +52,7 @@ private:
QMenu *m_exportTxKeyMenu; QMenu *m_exportTxKeyMenu;
QString m_txid; QString m_txid;
bool m_offline; bool m_offline;
bool m_multisig = false;
}; };
#endif //FEATHER_TXCONFADVDIALOG_H #endif //FEATHER_TXCONFADVDIALOG_H

View file

@ -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->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); connect(ui->btn_Advanced, &QPushButton::clicked, this, &TxConfDialog::setShowAdvanced);
this->adjustSize(); this->adjustSize();
} }
quint32 TxConfDialog::getMultisigSignerIndex() {
return ui->combo_sendTo->currentIndex();
}
void TxConfDialog::setShowAdvanced() { void TxConfDialog::setShowAdvanced() {
this->showAdvanced = true; this->showAdvanced = true;
QDialog::reject(); QDialog::reject();

View file

@ -23,6 +23,8 @@ public:
explicit TxConfDialog(Wallet *wallet, PendingTransaction *tx, const QString &address, const QString &description, QWidget *parent = nullptr); explicit TxConfDialog(Wallet *wallet, PendingTransaction *tx, const QString &address, const QString &description, QWidget *parent = nullptr);
~TxConfDialog() override; ~TxConfDialog() override;
quint32 getMultisigSignerIndex();
bool showAdvanced = false; bool showAdvanced = false;
private: private:

View file

@ -119,6 +119,16 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="0">
<widget class="QLabel" name="label_sendTo">
<property name="text">
<string>Send to:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QComboBox" name="combo_sendTo"/>
</item>
</layout> </layout>
</item> </item>
</layout> </layout>

View file

@ -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_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_description = m_wallet->getCacheAttribute(QString("coin.description:%1").arg(ci->m_pubKey));
ci->m_change = m_wallet2->is_change(td); 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); m_rows.push_back(ci);
} }

View file

@ -30,7 +30,6 @@ Q_OBJECT
public: public:
bool coin(int index, std::function<void (CoinsInfo &)> callback); bool coin(int index, std::function<void (CoinsInfo &)> callback);
CoinsInfo * coin(int index); CoinsInfo * coin(int index);
void refresh();
void refreshUnlocked(); void refreshUnlocked();
void freeze(QString &publicKey); void freeze(QString &publicKey);
void thaw(QString &publicKey); void thaw(QString &publicKey);
@ -41,6 +40,9 @@ public:
quint64 count() const; quint64 count() const;
void clearRows(); void clearRows();
public slots:
void refresh();
signals: signals:
void refreshStarted() const; void refreshStarted() const;
void refreshFinished() const; void refreshFinished() const;

File diff suppressed because it is too large Load diff

View file

@ -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 <functional>
#include <QObject>
#include <QList>
#include <QReadWriteLock>
#include <QDateTime>
#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<void (TxProposal &)> callback);
quint64 txProposalCount() const;
bool message(int index, std::function<void (MultisigMessage &)> 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<std::string> &kexMessages);
bool exchangeMultisig(const std::vector<std::string> &kexMessages);
bool exportMultisig();
bool importMultisig(const std::vector<cryptonote::blobdata> 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<SignerInfo> 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<MultisigMessage*> m_rows;
QList<TxProposal*> m_txProposals;
QHash<QString, quint64> m_txProposalsIndex;
// QList<TxProposal*> 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

View file

@ -53,6 +53,14 @@ QStringList PendingTransaction::txid() const
return list; 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 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<std::string> 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) PendingTransaction::PendingTransaction(Monero::PendingTransaction *pt, QObject *parent)
: QObject(parent) : QObject(parent)
, m_pimpl(pt) , m_pimpl(pt)

View file

@ -31,12 +31,23 @@ public:
quint64 dust() const; quint64 dust() const;
quint64 fee() const; quint64 fee() const;
QStringList txid() const; QStringList txid() const;
QStringList prefixHashes() const;
quint64 txCount() const; quint64 txCount() const;
QList<QVariant> subaddrIndices() const; QList<QVariant> subaddrIndices() const;
std::string unsignedTxToBin() const; std::string unsignedTxToBin() const;
QString unsignedTxToBase64() const; QString unsignedTxToBase64() const;
QString signedTxToHex(int index) const; QString signedTxToHex(int index) const;
void refresh(); 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; PendingTransactionInfo * transaction(int index) const;

View file

@ -185,6 +185,10 @@ void TransactionHistory::refresh()
t->m_rings.append(ring); 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); m_rows.append(t);
} }

View file

@ -8,6 +8,7 @@
#include "AddressBook.h" #include "AddressBook.h"
#include "Coins.h" #include "Coins.h"
#include "MultisigMessageStore.h"
#include "Subaddress.h" #include "Subaddress.h"
#include "SubaddressAccount.h" #include "SubaddressAccount.h"
#include "TransactionHistory.h" #include "TransactionHistory.h"
@ -22,6 +23,8 @@
#include "model/SubaddressModel.h" #include "model/SubaddressModel.h"
#include "model/SubaddressAccountModel.h" #include "model/SubaddressAccountModel.h"
#include "model/CoinsModel.h" #include "model/CoinsModel.h"
#include "model/MultisigMessageModel.h"
#include "model/MultisigIncomingTxModel.h"
#include "utils/ScopeGuard.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_subaddressAccount(new SubaddressAccount(this, wallet->getWallet(), this))
, m_refreshNow(false) , m_refreshNow(false)
, m_refreshEnabled(false) , m_refreshEnabled(false)
, m_mmsRefreshEnabled(false)
, m_scheduler(this) , m_scheduler(this)
, m_useSSL(true) , m_useSSL(true)
, m_coins(new Coins(this, wallet->getWallet(), this)) , m_coins(new Coins(this, wallet->getWallet(), this))
, m_mmsStore(new MultisigMessageStore(this, wallet->getWallet(), this))
, m_storeTimer(new QTimer(this)) , m_storeTimer(new QTimer(this))
{ {
m_walletListener = new WalletListenerImpl(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_subaddressModel = new SubaddressModel(this, m_subaddress);
m_subaddressAccountModel = new SubaddressAccountModel(this, m_subaddressAccount); m_subaddressAccountModel = new SubaddressAccountModel(this, m_subaddressAccount);
m_coinsModel = new CoinsModel(this, m_coins); 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) { if (this->status() == Status_Ok) {
startRefreshThread(); startRefreshThread();
@ -86,6 +93,9 @@ Wallet::Wallet(Monero::Wallet *wallet, QObject *parent)
connect(m_subaddress, &Subaddress::corrupted, [this]{ connect(m_subaddress, &Subaddress::corrupted, [this]{
emit keysCorrupted(); emit keysCorrupted();
}); });
connect(m_mmsStore, &MultisigMessageStore::multisigInfoImported, m_coins, &Coins::refresh);
connect(m_mmsStore, &MultisigMessageStore::multisigInfoExported, m_coins, &Coins::refresh);
} }
// #################### Status #################### // #################### Status ####################
@ -128,6 +138,12 @@ bool Wallet::viewOnly() const {
return m_wallet2->watch_only(); return m_wallet2->watch_only();
} }
bool Wallet::isMultisig() const {
bool ready, multisig;
multisig = m_wallet2->multisig(&ready);
return multisig;
}
bool Wallet::isDeterministic() const { bool Wallet::isDeterministic() const {
return m_wallet2->is_deterministic(); 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())); 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 { qsizetype Wallet::seedLength() const {
auto seedLength = this->getCacheAttribute("feather.seed").split(" ", Qt::SkipEmptyParts).length(); auto seedLength = this->getCacheAttribute("feather.seed").split(" ", Qt::SkipEmptyParts).length();
return seedLength ? seedLength : 25; return seedLength ? seedLength : 25;
@ -420,47 +442,54 @@ void Wallet::startRefreshThread()
const auto future = m_scheduler.run([this] { const auto future = m_scheduler.run([this] {
// Beware! This code does not run in the GUI thread. // Beware! This code does not run in the GUI thread.
constexpr const std::chrono::seconds refreshInterval{10};
constexpr const std::chrono::milliseconds intervalResolution{100}; constexpr const std::chrono::milliseconds intervalResolution{100};
auto last = std::chrono::steady_clock::now(); auto last = std::chrono::steady_clock::now();
while (!m_scheduler.stopping()) while (!m_scheduler.stopping())
{ {
if (m_refreshEnabled && (!isHwBacked() || isDeviceConnected())) if ((!isHwBacked() || isDeviceConnected()))
{ {
const auto now = std::chrono::steady_clock::now(); const auto now = std::chrono::steady_clock::now();
const auto elapsed = now - last; 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 // get daemonHeight and targetHeight
// daemonHeight and targetHeight will be 0 if call to get_info fails // daemonHeight and targetHeight will be 0 if call to get_info fails
quint64 daemonHeight = m_walletImpl->daemonBlockChainHeight(); quint64 daemonHeight = m_walletImpl->daemonBlockChainHeight();
bool success = daemonHeight > 0; bool success = daemonHeight > 0;
quint64 targetHeight = 0; quint64 targetHeight = 0;
if (success) { if (success) {
targetHeight = m_walletImpl->daemonBlockChainTargetHeight(); 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;
} }
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() { void Wallet::onUpdated() {
this->updateBalance(); this->updateBalance();
if (this->isSynchronized()) { if (this->isSynchronized()) {
// m_mmsStore->exportMultisig();
this->refreshModels(); this->refreshModels();
} }
} }
@ -563,6 +593,11 @@ void Wallet::refreshModels() {
m_history->refresh(); m_history->refresh();
m_coins->refresh(); m_coins->refresh();
this->subaddress()->refresh(this->currentSubaddressAccount()); this->subaddress()->refresh(this->currentSubaddressAccount());
m_mmsStore->refresh();
}
void Wallet::setRefreshInterval(qint64 seconds) {
m_refreshInterval = std::chrono::seconds{seconds};
} }
// #################### Hardware wallet #################### // #################### Hardware wallet ####################
@ -1023,6 +1058,13 @@ PendingTransaction * Wallet::loadSignedTxFromStr(const std::string &data)
return result; 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 bool Wallet::submitTxFile(const QString &fileName) const
{ {
qDebug() << "Trying to submit " << fileName; qDebug() << "Trying to submit " << fileName;
@ -1103,6 +1145,18 @@ CoinsModel* Wallet::coinsModel() const {
return m_coinsModel; 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 #################### // #################### Transaction proofs ####################
QString Wallet::getTxKey(const QString &txid) const { 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); 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 #################### // #################### Misc / Unused ####################
quint64 Wallet::getBytesReceived() const { quint64 Wallet::getBytesReceived() const {

View file

@ -71,6 +71,9 @@ class SubaddressAccount;
class SubaddressAccountModel; class SubaddressAccountModel;
class Coins; class Coins;
class CoinsModel; class CoinsModel;
class MultisigMessageStore;
class MultisigMessageModel;
class MultisigIncomingTxModel;
struct TxProofResult { struct TxProofResult {
TxProofResult() {} TxProofResult() {}
@ -133,6 +136,8 @@ public:
//! returns if view only wallet //! returns if view only wallet
bool viewOnly() const; bool viewOnly() const;
bool isMultisig() const;
//! return true if deterministic keys //! return true if deterministic keys
bool isDeterministic() const; bool isDeterministic() const;
@ -175,6 +180,7 @@ public:
//! returns mnemonic seed //! returns mnemonic seed
QString getSeed(const QString &seedOffset) const; QString getSeed(const QString &seedOffset) const;
QString getMultisigSeed() const;
qsizetype seedLength() const; qsizetype seedLength() const;
@ -227,6 +233,8 @@ public:
void refreshModels(); void refreshModels();
void setRefreshInterval(qint64 seconds);
// ##### Hardware wallet ##### // ##### Hardware wallet #####
bool isHwBacked() const; bool isHwBacked() const;
bool isLedger() const; bool isLedger() const;
@ -342,6 +350,8 @@ public:
PendingTransaction * loadSignedTxFile(const QString &fileName); PendingTransaction * loadSignedTxFile(const QString &fileName);
PendingTransaction * loadSignedTxFromStr(const std::string &data); PendingTransaction * loadSignedTxFromStr(const std::string &data);
PendingTransaction * restoreMultisigTransaction(const std::string &data);
//! Submit a transfer from file //! Submit a transfer from file
bool submitTxFile(const QString &fileName) const; bool submitTxFile(const QString &fileName) const;
@ -359,6 +369,9 @@ public:
SubaddressAccountModel* subaddressAccountModel() const; SubaddressAccountModel* subaddressAccountModel() const;
Coins* coins() const; Coins* coins() const;
CoinsModel* coinsModel() const; CoinsModel* coinsModel() const;
MultisigMessageModel* mmsModel() const;
MultisigIncomingTxModel* msIncomingTxModel() const;
MultisigMessageStore* mmsStore() const;
// ##### Transaction proofs ##### // ##### Transaction proofs #####
@ -386,6 +399,20 @@ public:
QString make_uri(const QString &address, quint64 &amount, const QString &description, const QString &recipient) const; 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 ##### // ##### Misc / Unused #####
quint64 getBytesReceived() const; quint64 getBytesReceived() const;
@ -498,6 +525,10 @@ private:
Coins *m_coins; Coins *m_coins;
CoinsModel *m_coinsModel; CoinsModel *m_coinsModel;
MultisigMessageStore *m_mmsStore;
MultisigMessageModel *m_mmsModel;
MultisigIncomingTxModel *m_msIncomingTxModel;
QMutex m_asyncMutex; QMutex m_asyncMutex;
QString m_daemonUsername; QString m_daemonUsername;
QString m_daemonPassword; QString m_daemonPassword;
@ -505,6 +536,8 @@ private:
QMutex m_proxyMutex; QMutex m_proxyMutex;
std::atomic<bool> m_refreshNow; std::atomic<bool> m_refreshNow;
std::atomic<bool> m_refreshEnabled; std::atomic<bool> m_refreshEnabled;
std::atomic<bool> m_mmsRefreshEnabled;
std::chrono::seconds m_refreshInterval{10};
WalletListenerImpl *m_walletListener; WalletListenerImpl *m_walletListener;
FutureScheduler m_scheduler; FutureScheduler m_scheduler;

View file

@ -4,6 +4,7 @@
#include "WalletListenerImpl.h" #include "WalletListenerImpl.h"
#include "Wallet.h" #include "Wallet.h"
#include "WalletManager.h" #include "WalletManager.h"
#include "MultisigMessageStore.h"
WalletListenerImpl::WalletListenerImpl(Wallet * w) WalletListenerImpl::WalletListenerImpl(Wallet * w)
: m_wallet(w) : m_wallet(w)
@ -31,6 +32,11 @@ void WalletListenerImpl::moneyReceived(const std::string &txId, uint64_t amount)
QString qTxId = QString::fromStdString(txId); QString qTxId = QString::fromStdString(txId);
qDebug() << Q_FUNC_INFO << qTxId << " " << WalletManager::displayAmount(amount); 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); emit m_wallet->moneyReceived(qTxId, amount);
} }

View file

@ -95,9 +95,12 @@ Wallet *WalletManager::openWallet(const QString &path, const QString &password,
auto wallet = new Wallet(w); auto wallet = new Wallet(w);
// move wallet to the GUI thread. Otherwise it wont be emitting signals // move wallet to the GUI thread. Otherwise it wont be emitting signals
qDebug() << wallet->thread();
qDebug() << qApp->thread();
if (wallet->thread() != qApp->thread()) { if (wallet->thread() != qApp->thread()) {
wallet->moveToThread(qApp->thread()); wallet->moveToThread(qApp->thread());
} }
qDebug() << wallet->thread();
return wallet; 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<Monero::NetworkType>(nettype), restoreHeight, multisigSeed.toStdString(), mmsRecovery.toStdString(), kdfRounds, subaddressLookahead.toStdString());
return new Wallet(w);
}
bool WalletManager::walletExists(const QString &path) const bool WalletManager::walletExists(const QString &path) const
{ {
return m_pimpl->walletExists(path.toStdString()); return m_pimpl->walletExists(path.toStdString());

View file

@ -58,6 +58,15 @@ public:
const QString &subaddressLookahead = "" 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, Wallet * createDeterministicWalletFromSpendKey(const QString &path,
const QString &password, const QString &password,
const QString &language, const QString &language,

View file

@ -124,6 +124,18 @@ QString CoinsInfo::txNote() const {
return m_txNote; 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) CoinsInfo::CoinsInfo(QObject *parent)
: QObject(parent) : QObject(parent)
, m_blockHeight(0) , m_blockHeight(0)
@ -142,6 +154,8 @@ CoinsInfo::CoinsInfo(QObject *parent)
, m_unlocked(false) , m_unlocked(false)
, m_coinbase(false) , m_coinbase(false)
, m_change(false) , m_change(false)
, m_keyImagePartial(false)
, m_haveMultisigK(false)
{ {
} }

View file

@ -39,6 +39,9 @@ public:
QString description() const; QString description() const;
bool change() const; bool change() const;
QString txNote() const; QString txNote() const;
bool keyImagePartial() const;
bool haveMultisigK() const;
QStringList multisigInfo() const;
void setUnlocked(bool unlocked); void setUnlocked(bool unlocked);
@ -71,6 +74,9 @@ private:
QString m_description; QString m_description;
bool m_change; bool m_change;
QString m_txNote; QString m_txNote;
bool m_keyImagePartial;
bool m_haveMultisigK;
QStringList m_multisigInfo;
}; };
#endif //FEATHER_COINSINFO_H #endif //FEATHER_COINSINFO_H

View file

@ -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)
{
}

View file

@ -0,0 +1,56 @@
//
// Created by user on 1/3/24.
//
#ifndef FEATHER_MULTISIGMESSAGE_H
#define FEATHER_MULTISIGMESSAGE_H
#include <QObject>
#include <QDateTime>
//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

View file

@ -165,3 +165,7 @@ QString TransactionRow::rings_formatted() const
} }
return rings; return rings;
} }
QString TransactionRow::prefixHash() const {
return m_prefixHash;
}

View file

@ -50,6 +50,7 @@ public:
QList<QString> destinations() const; QList<QString> destinations() const;
QList<Transfer*> transfers() const; QList<Transfer*> transfers() const;
QString rings_formatted() const; QString rings_formatted() const;
QString prefixHash() const;
private: private:
explicit TransactionRow(); explicit TransactionRow();
@ -76,6 +77,7 @@ private:
QDateTime m_timestamp; QDateTime m_timestamp;
quint64 m_unlockTime; quint64 m_unlockTime;
bool m_coinbase; bool m_coinbase;
QString m_prefixHash;
}; };

View file

@ -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 "";
}

View file

@ -0,0 +1,47 @@
//
// Created by user on 3/20/24.
//
#ifndef FEATHER_TXPROPOSAL_H
#define FEATHER_TXPROPOSAL_H
#include <QObject>
#include <QDateTime>
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

View file

@ -5,7 +5,7 @@
#include <QApplication> #include <QApplication>
#include <QtCore> #include <QtCore>
#include <QtGui> #include <QtGui>
#include <singleapplication.h> //#include <singleapplication.h>
#include "config-feather.h" #include "config-feather.h"
#include "constants.h" #include "constants.h"
@ -88,6 +88,11 @@ if (AttachConsole(ATTACH_PARENT_PROCESS)) {
qputenv("QT_QPA_PLATFORM", "windows:darkmode=1"); qputenv("QT_QPA_PLATFORM", "windows:darkmode=1");
#endif #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_; QStringList argv_;
for(int i = 0; i != argc; i++){ for(int i = 0; i != argc; i++){
argv_ << QString::fromStdString(argv[i]); argv_ << QString::fromStdString(argv[i]);
@ -137,7 +142,7 @@ if (AttachConsole(ATTACH_PARENT_PROCESS)) {
QApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::Round); QApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::Round);
#endif #endif
SingleApplication app(argc, argv); QApplication app(argc, argv);
QApplication::setQuitOnLastWindowClosed(false); QApplication::setQuitOnLastWindowClosed(false);
QApplication::setApplicationName("FeatherWallet"); QApplication::setApplicationName("FeatherWallet");
@ -243,10 +248,11 @@ if (AttachConsole(ATTACH_PARENT_PROCESS)) {
auto wm = windowManager(); auto wm = windowManager();
wm->setEventFilter(&filter); wm->setEventFilter(&filter);
wm->raise();
QObject::connect(&app, &SingleApplication::instanceStarted, [&wm]() { // QObject::connect(&app, &SingleApplication::instanceStarted, [&wm]() {
wm->raise(); // wm->raise();
}); // });
return QApplication::exec(); return QApplication::exec();
} }

View file

@ -90,12 +90,22 @@ QVariant CoinsModel::data(const QModelIndex &index, int role) const
switch (index.column()) { switch (index.column()) {
case KeyImageKnown: case KeyImageKnown:
{ {
if (cInfo.keyImageKnown()) { if (!cInfo.keyImageKnown() || cInfo.keyImagePartial()) {
result = QVariant(icons()->icon("eye1.png"));
}
else {
result = QVariant(icons()->icon("eye_blind.png")); 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 { } else {
result = "Key image unknown. Outgoing transactions that include this output will not be detected."; 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()) { if (cInfo.frozen()) {
@ -148,6 +173,8 @@ QVariant CoinsModel::headerData(int section, Qt::Orientation orientation, int ro
switch(section) { switch(section) {
case PubKey: case PubKey:
return QString("Pub Key"); return QString("Pub Key");
case MultisigInfo:
return QString("Multisig info");
case TxID: case TxID:
return QString("TxID"); return QString("TxID");
case BlockHeight: case BlockHeight:
@ -217,6 +244,10 @@ QVariant CoinsModel::parseTransactionInfo(const CoinsInfo &cInfo, int column, in
{ {
case KeyImageKnown: case KeyImageKnown:
return ""; return "";
case HaveMultisigK:
return "";
case MultisigInfo:
return cInfo.multisigInfo().join(", ");
case PubKey: case PubKey:
return cInfo.pubKey().mid(0,8); return cInfo.pubKey().mid(0,8);
case TxID: case TxID:

View file

@ -22,6 +22,8 @@ public:
enum ModelColumn enum ModelColumn
{ {
KeyImageKnown = 0, KeyImageKnown = 0,
HaveMultisigK,
MultisigInfo,
PubKey, PubKey,
TxID, TxID,
Address, Address,

View file

@ -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 <QBrush>
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<quint64>(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();
}
}
}

View file

@ -0,0 +1,56 @@
////
//// Created by user on 1/9/24.
////
//
#ifndef FEATHER_MULTISIGINCOMINGTXMODEL_H
#define FEATHER_MULTISIGINCOMINGTXMODEL_H
#include <wallet/api/wallet2_api.h>
#include <QAbstractTableModel>
#include <QSortFilterProxyModel>
#include <QDebug>
#include <QIcon>
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

View file

@ -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 <QBrush>
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<quint64>(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();
}
}
}

View file

@ -0,0 +1,58 @@
//
// Created by user on 1/3/24.
//
#ifndef FEATHER_MULTISIGMESSAGEMODEL_H
#define FEATHER_MULTISIGMESSAGEMODEL_H
#include <wallet/api/wallet2_api.h>
#include <QAbstractTableModel>
#include <QSortFilterProxyModel>
#include <QDebug>
#include <QIcon>
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

View file

@ -165,6 +165,10 @@ QVariant TransactionHistoryModel::parseTransactionInfo(const TransactionRow &tIn
amount = (tInfo.balanceDelta() < 0) ? amount : "+" + amount; amount = (tInfo.balanceDelta() < 0) ? amount : "+" + amount;
return amount; return amount;
} }
// case Column::TxPrefixHash:
// {
// return tInfo.prefixHash().mid(0, 8);
// }
case Column::TxID: { case Column::TxID: {
if (conf()->get(Config::historyShowFullTxid).toBool()) { if (conf()->get(Config::historyShowFullTxid).toBool()) {
return tInfo.hash(); return tInfo.hash();
@ -217,6 +221,8 @@ QVariant TransactionHistoryModel::headerData(int section, Qt::Orientation orient
return QString("Amount"); return QString("Amount");
case Column::TxID: case Column::TxID:
return QString("Txid"); return QString("Txid");
// case Column::TxPrefixHash:
// return QString("Prefix hash");
case Column::FiatAmount: case Column::FiatAmount:
return QString("Fiat"); return QString("Fiat");
default: default:

View file

@ -513,6 +513,14 @@ QString displayAddress(const QString& address, int sections, const QString& sep)
return list.join(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 addressTextFormat(const SubaddressIndex &index, quint64 amount) {
QTextCharFormat rec; QTextCharFormat rec;
if (index.isPrimary()) { if (index.isPrimary()) {
@ -684,4 +692,13 @@ QString formatSyncStatus(quint64 height, quint64 target, bool daemonSync) {
return "Synchronized"; 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"));
}
} }

View file

@ -89,6 +89,7 @@ namespace Utils
void externalLinkWarning(QWidget *parent, const QString &url); void externalLinkWarning(QWidget *parent, const QString &url);
QString displayAddress(const QString& address, int sections = 3, const QString & sep = " "); QString displayAddress(const QString& address, int sections = 3, const QString & sep = " ");
QString chunkAddress(const QString& address);
QTextCharFormat addressTextFormat(const SubaddressIndex &index, quint64 amount); QTextCharFormat addressTextFormat(const SubaddressIndex &index, quint64 amount);
QFont getMonospaceFont(); QFont getMonospaceFont();
@ -116,6 +117,7 @@ namespace Utils
void clearLayout(QLayout *layout, bool deleteWidgets = true); void clearLayout(QLayout *layout, bool deleteWidgets = true);
QString formatSyncStatus(quint64 height, quint64 target, bool daemonSync = false); QString formatSyncStatus(quint64 height, quint64 target, bool daemonSync = false);
QString formatRestoreHeight(Wallet *wallet);
} }
#endif //FEATHER_UTILS_H #endif //FEATHER_UTILS_H

View file

@ -21,6 +21,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
{Config::warnOnStagenet,{QS("warnOnStagenet"), true}}, {Config::warnOnStagenet,{QS("warnOnStagenet"), true}},
{Config::warnOnTestnet,{QS("warnOnTestnet"), true}}, {Config::warnOnTestnet,{QS("warnOnTestnet"), true}},
{Config::warnOnKiImport,{QS("warnOnKiImport"), true}}, {Config::warnOnKiImport,{QS("warnOnKiImport"), true}},
{Config::warnOnMultisigExperimental,{QS("warnOnMultisigExperimental"), true}},
{Config::logLevel,{QS("logLevel"), 0}}, {Config::logLevel,{QS("logLevel"), 0}},
{Config::homeWidget,{QS("homeWidget"), "ccs"}}, {Config::homeWidget,{QS("homeWidget"), "ccs"}},

View file

@ -25,6 +25,7 @@ public:
warnOnStagenet, warnOnStagenet,
warnOnTestnet, warnOnTestnet,
warnOnKiImport, warnOnKiImport,
warnOnMultisigExperimental,
homeWidget, homeWidget,
donateBeg, donateBeg,

View file

@ -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;

View file

@ -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 <QItemSelection>
#include <QTreeView>
#include <QWidget>
#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::MultisigSetupWidget> ui;
};
#endif //FEATHER_MULTISIGSETUPWIDGET_H

View file

@ -0,0 +1,415 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MultisigSetupWidget</class>
<widget class="QWidget" name="MultisigSetupWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>720</width>
<height>361</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QStackedWidget" name="stackedWidget">
<property name="currentIndex">
<number>1</number>
</property>
<widget class="QWidget" name="page_3">
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QLabel" name="label_8">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Create setup key&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_11">
<property name="text">
<string>Click 'Next' if you already have a setup key.</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Threshold:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>Signers:</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_12">
<property name="text">
<string>Setup key:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QLineEdit" name="line_generatedSetupKey">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_copySetupKey">
<property name="text">
<string>Copy</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_13">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>
<widget class="QSpinBox" name="spin_threshold">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>16</number>
</property>
<property name="value">
<number>2</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_14">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Number of signatures required to spend funds.</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="spin_signers">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimum">
<number>2</number>
</property>
<property name="maximum">
<number>16</number>
</property>
<property name="value">
<number>3</number>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPushButton" name="btn_generateSetupKey">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Generate</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="page">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Multisig setup&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Setup key:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="line_setupKey"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Channel:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLineEdit" name="line_channel"/>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Username:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="line_name"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Threshold:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_threshold">
<property name="text">
<string>?</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Signers:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_signers">
<property name="text">
<string>?</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_2">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Waiting for participants&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item>
<widget class="QTreeWidget" name="tree_participants">
<column>
<property name="text">
<string>Name</string>
</property>
</column>
<column>
<property name="text">
<string>Address</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_4">
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QLabel" name="label_15">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;Multisig wallet created successfully&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item>
<layout class="QFormLayout" name="formLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label_16">
<property name="text">
<string>Address:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="lineEdit_2"/>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label_17">
<property name="text">
<string>All participants must verify that the address matches via a secure channel.</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_18">
<property name="text">
<string>Do not proceed until you are certain that every signer has the same address.</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="btn_next">
<property name="text">
<string>Next</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -72,6 +72,20 @@ void TxDetailsSimple::setDetails(Wallet *wallet, PendingTransaction *tx, const Q
ui->label_fee->setStyleSheet(ColorScheme::RED.asStylesheet(true)); ui->label_fee->setStyleSheet(ColorScheme::RED.asStylesheet(true));
ui->label_fee->setToolTip("Unrealistic fee. You may be connected to a malicious node."); 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; TxDetailsSimple::~TxDetailsSimple() = default;

View file

@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>386</width> <width>576</width>
<height>152</height> <height>230</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -109,6 +109,67 @@
</item> </item>
</layout> </layout>
</item> </item>
<item>
<widget class="QFrame" name="frame_multisig">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QFormLayout" name="formLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Signatures:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label_signatures">
<property name="text">
<string>1/?</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Send to:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="combo_sendTo">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<resources/> <resources/>

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PageCreateWalletType</class>
<widget class="QWizardPage" name="PageCreateWalletType">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<height>417</height>
</rect>
</property>
<property name="windowTitle">
<string>WizardPage</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Select wallet type to create:</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QRadioButton" name="radioButton">
<property name="text">
<string>Standard</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radioButton_2">
<property name="text">
<string>Multisig</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>275</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -16,14 +16,13 @@ PageHardwareDevice::PageHardwareDevice(WizardFields *fields, QWidget *parent)
{ {
ui->setupUi(this); ui->setupUi(this);
ui->combo_deviceType->addItem("Ledger Nano S (PLUS) / X", DeviceType::LEDGER); this->setTitle("Restore from hardware device");
ui->combo_deviceType->addItem("Trezor Model T / Safe 3", DeviceType::TREZOR);
connect(ui->btnOptions, &QPushButton::clicked, this, &PageHardwareDevice::onOptionsClicked); connect(ui->btnOptions, &QPushButton::clicked, this, &PageHardwareDevice::onOptionsClicked);
} }
void PageHardwareDevice::initializePage() { void PageHardwareDevice::initializePage() {
ui->radioNewWallet->setChecked(true); ui->radio_create->setChecked(true);
} }
int PageHardwareDevice::nextId() const { int PageHardwareDevice::nextId() const {
@ -35,8 +34,13 @@ int PageHardwareDevice::nextId() const {
} }
bool PageHardwareDevice::validatePage() { bool PageHardwareDevice::validatePage() {
m_fields->deviceType = static_cast<DeviceType>(ui->combo_deviceType->currentData().toInt()); if (ui->radio_ledger->isChecked()) {
m_fields->showSetRestoreHeightPage = ui->radioRestoreWallet->isChecked(); m_fields->deviceType = DeviceType::LEDGER;
} else {
m_fields->deviceType = DeviceType::TREZOR;
}
m_fields->showSetRestoreHeightPage = ui->radio_restore->isChecked();
return true; return true;
} }

View file

@ -15,15 +15,31 @@
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout">
<item> <item>
<widget class="QLabel" name="label"> <widget class="QGroupBox" name="groupBox">
<property name="text"> <property name="title">
<string>Select device type:</string> <string>Select device type: </string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QRadioButton" name="radio_ledger">
<property name="text">
<string>Ledger (Nano S, Nano S+, Nano X)</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radio_trezor">
<property name="text">
<string>Trezor (Model T, Safe 3)</string>
</property>
</widget>
</item>
</layout>
</widget> </widget>
</item> </item>
<item>
<widget class="QComboBox" name="combo_deviceType"/>
</item>
<item> <item>
<widget class="Line" name="line"> <widget class="Line" name="line">
<property name="orientation"> <property name="orientation">
@ -32,79 +48,31 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QRadioButton" name="radioNewWallet"> <widget class="QGroupBox" name="groupBox_2">
<property name="text"> <property name="title">
<string>Create a new wallet file from device</string> <string>Does the wallet currently hold any funds? </string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QRadioButton" name="radio_create">
<property name="text">
<string>No</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radio_restore">
<property name="text">
<string>Yes</string>
</property>
</widget>
</item>
</layout>
</widget> </widget>
</item> </item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Use this option if the keys on the device hold no funds. (i.e. this wallet was never used before)</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QRadioButton" name="radioRestoreWallet">
<property name="text">
<string>Restore a wallet from device</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>If this option is selected, you will be asked specify a wallet creation date or restore height next.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item> <item>
<spacer name="verticalSpacer"> <spacer name="verticalSpacer">
<property name="orientation"> <property name="orientation">

View file

@ -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 <QFileDialog>
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;
}

32
src/wizard/PageKeyType.h Normal file
View file

@ -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 <QLabel>
#include <QWizardPage>
#include <QWidget>
#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

67
src/wizard/PageKeyType.ui Normal file
View file

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PageKeyType</class>
<widget class="QWizardPage" name="PageKeyType">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>485</width>
<height>329</height>
</rect>
</property>
<property name="windowTitle">
<string>WizardPage</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Select key type:</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QRadioButton" name="radio_seed">
<property name="text">
<string>Seed</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radio_deterministic">
<property name="text">
<string>Spendkey (deterministic)</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radio_nondeterministic">
<property name="text">
<string>Spendkey + Viewkey (non-deterministic)</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -21,13 +21,34 @@ PageMenu::PageMenu(WizardFields *fields, WalletKeysFilesModel *wallets, QWidget
ui->label_version->setText(QString("Feather %1 — by dsc & tobtoht").arg(FEATHER_VERSION)); ui->label_version->setText(QString("Feather %1 — by dsc & tobtoht").arg(FEATHER_VERSION));
QString settingsSkin = conf()->get(Config::skin).toString(); 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() { void PageMenu::initializePage() {
if (m_walletKeysFilesModel->rowCount() > 0) { if (m_walletKeysFilesModel->rowCount() > 0) {
ui->radioOpen->setChecked(true); ui->radio_open->setChecked(true);
} else { } else {
ui->radioCreate->setChecked(true); ui->radio_create->setChecked(true);
} }
// Don't show setup wizard again // Don't show setup wizard again
@ -35,15 +56,19 @@ void PageMenu::initializePage() {
} }
int PageMenu::nextId() const { int PageMenu::nextId() const {
if (ui->radioCreate->isChecked()) if (ui->radio_create->isChecked())
return WalletWizard::Page_CreateWalletSeed; 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; return WalletWizard::Page_OpenWallet;
if (ui->radioSeed->isChecked()) if (ui->radio_restoreSeed->isChecked())
return WalletWizard::Page_WalletRestoreSeed; return WalletWizard::Page_WalletRestoreSeed;
if (ui->radioViewOnly->isChecked()) if (ui->radio_restoreKeys->isChecked())
return WalletWizard::Page_WalletRestoreKeys; 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 WalletWizard::Page_HardwareDevice;
return 0; return 0;
} }
@ -51,23 +76,31 @@ int PageMenu::nextId() const {
bool PageMenu::validatePage() { bool PageMenu::validatePage() {
m_fields->clearFields(); m_fields->clearFields();
if (ui->radioCreate->isChecked()) { if (ui->radio_create->isChecked()) {
m_fields->mode = WizardMode::CreateWallet; m_fields->mode = WizardMode::CreateWallet;
m_fields->modeText = "Create wallet"; 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->mode = WizardMode::OpenWallet;
m_fields->modeText = "Open wallet"; m_fields->modeText = "Open wallet";
} }
if (ui->radioSeed->isChecked()) { if (ui->radio_restoreSeed->isChecked()) {
m_fields->mode = WizardMode::RestoreFromSeed; m_fields->mode = WizardMode::RestoreFromSeed;
m_fields->modeText = "Restore wallet"; m_fields->modeText = "Restore wallet";
} }
if (ui->radioViewOnly->isChecked()) { if (ui->radio_restoreKeys->isChecked()) {
m_fields->mode = WizardMode::RestoreFromKeys; m_fields->mode = WizardMode::RestoreFromKeys;
m_fields->modeText = "Restore wallet"; 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->mode = WizardMode::CreateWalletFromDevice;
m_fields->modeText = "Create from hardware device"; m_fields->modeText = "Create from hardware device";
} }

View file

@ -26,6 +26,8 @@ private:
Ui::PageMenu *ui; Ui::PageMenu *ui;
WalletKeysFilesModel *m_walletKeysFilesModel; WalletKeysFilesModel *m_walletKeysFilesModel;
WizardFields *m_fields; WizardFields *m_fields;
WalletWizard::Page m_nextPage;
}; };
#endif //FEATHER_WIZARDMENU_H #endif //FEATHER_WIZARDMENU_H

View file

@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>617</width> <width>610</width>
<height>463</height> <height>465</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -15,63 +15,92 @@
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout">
<item> <item>
<widget class="QLabel" name="label"> <widget class="QGroupBox" name="groupBox">
<property name="minimumSize"> <property name="title">
<size> <string/>
<width>400</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Select option:</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radioCreate">
<property name="focusPolicy">
<enum>Qt::ClickFocus</enum>
</property>
<property name="text">
<string>Create new wallet</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radioOpen">
<property name="focusPolicy">
<enum>Qt::ClickFocus</enum>
</property>
<property name="text">
<string>Open wallet file</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radioSeed">
<property name="focusPolicy">
<enum>Qt::ClickFocus</enum>
</property>
<property name="text">
<string>Restore wallet from seed</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radioViewOnly">
<property name="focusPolicy">
<enum>Qt::ClickFocus</enum>
</property>
<property name="text">
<string>Restore wallet from keys</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radioCreateFromDevice">
<property name="text">
<string>Create wallet from hardware device</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Create a new..</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radio_create">
<property name="text">
<string>Standard wallet</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radio_createMultisig">
<property name="text">
<string>Multisig wallet</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radio_open">
<property name="text">
<string>Open wallet file</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Restore wallet from..</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radio_restoreSeed">
<property name="text">
<string>Seed</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radio_restoreKeys">
<property name="text">
<string>Keys</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radio_restoreMultisig">
<property name="text">
<string>Multisig seed</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radio_restoreHardware">
<property name="text">
<string>Hardware device</string>
</property>
</widget>
</item>
</layout>
</widget> </widget>
</item> </item>
<item> <item>
@ -82,24 +111,17 @@
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>20</width> <width>20</width>
<height>40</height> <height>73</height>
</size> </size>
</property> </property>
</spacer> </spacer>
</item> </item>
<item> <item>
<layout class="QVBoxLayout" name="verticalLayout_2"> <widget class="QLabel" name="label_version">
<item> <property name="text">
<widget class="QLabel" name="label_version"> <string>TextLabel</string>
<property name="enabled"> </property>
<bool>false</bool> </widget>
</property>
<property name="text">
<string>by dsc &amp; tobtoht</string>
</property>
</widget>
</item>
</layout>
</item> </item>
</layout> </layout>
</widget> </widget>

View file

@ -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 <QFileDialog>
#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;
}

View file

@ -0,0 +1,33 @@
//
// Created by user on 3/2/24.
//
#ifndef FEATHER_PAGERECOVERWALLET_H
#define FEATHER_PAGERECOVERWALLET_H
#include <QLabel>
#include <QWizardPage>
#include <QWidget>
#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

View file

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PageRecoverWallet</class>
<widget class="QWizardPage" name="PageRecoverWallet">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>589</width>
<height>404</height>
</rect>
</property>
<property name="windowTitle">
<string>WizardPage</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Select wallet type: </string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QRadioButton" name="radio_standard">
<property name="text">
<string>Standard</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radio_viewOnly">
<property name="text">
<string>View-only</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radio_multisig">
<property name="text">
<string>Multisig</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radio_hardware">
<property name="text">
<string>Hardware wallet</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -13,29 +13,42 @@ PageSetPassword::PageSetPassword(WizardFields *fields, QWidget *parent)
, m_fields(fields) , m_fields(fields)
{ {
ui->setupUi(this); ui->setupUi(this);
this->setFinalPage(true);
ui->frame_password->setInfo(icons()->icon("lock"), "Choose a password to encrypt your wallet keys."); ui->frame_password->setInfo(icons()->icon("lock"), "Choose a password to encrypt your wallet keys.");
connect(ui->widget_password, &PasswordSetWidget::passwordEntryChanged, [this]{ connect(ui->widget_password, &PasswordSetWidget::passwordEntryChanged, [this]{
this->completeChanged(); this->completeChanged();
}); });
this->setButtonText(QWizard::FinishButton, "Create/Open wallet");
} }
void PageSetPassword::initializePage() { 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); this->setTitle(m_fields->modeText);
ui->widget_password->resetFields(); ui->widget_password->resetFields();
} }
bool PageSetPassword::validatePage() { bool PageSetPassword::validatePage() {
m_fields->password = ui->widget_password->password(); 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; return true;
} }
int PageSetPassword::nextId() const { int PageSetPassword::nextId() const {
if (m_fields->mode == WizardMode::CreateMultisig) {
return WalletWizard::Page_MultisigCreateSetupKey;
}
return -1; return -1;
} }

View file

@ -26,10 +26,12 @@ public:
signals: signals:
void createWallet(); void createWallet();
void createMultisigWallet();
private: private:
Ui::PageSetPassword *ui; Ui::PageSetPassword *ui;
bool m_walletCreated = false;
WizardFields *m_fields; WizardFields *m_fields;
}; };

View file

@ -120,6 +120,9 @@ QString PageWalletFile::defaultWalletName() {
walletStr = QString("trezor_%1"); walletStr = QString("trezor_%1");
} }
} }
else if (m_fields->mode == WizardMode::CreateMultisig || m_fields->mode == WizardMode::RestoreMultisig) {
walletStr = QString("multisig_%1");
}
walletName = walletStr.arg(count); walletName = walletStr.arg(count);
count++; count++;
} while (this->walletPathExists(walletName)); } while (this->walletPathExists(walletName));

View file

@ -77,6 +77,7 @@ PageWalletRestoreKeys::PageWalletRestoreKeys(WizardFields *fields, QWidget *pare
} }
void PageWalletRestoreKeys::initializePage() { void PageWalletRestoreKeys::initializePage() {
ui->stackedWidget->setCurrentIndex(0);
this->showInputLines(); this->showInputLines();
} }
@ -97,12 +98,18 @@ void PageWalletRestoreKeys::showInputLines() {
ui->frame_viewKey->hide(); ui->frame_viewKey->hide();
ui->frame_spendKey->show(); ui->frame_spendKey->show();
} }
else { else if (ui->combo_walletType->currentIndex() == walletType::Spendable_Nondeterministic){
ui->frame_address->show(); ui->frame_address->show();
ui->frame_viewKey->show(); ui->frame_viewKey->show();
ui->frame_spendKey->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_address->setText("");
ui->line_viewkey->setText(""); ui->line_viewkey->setText("");
ui->line_spendkey->setText(""); ui->line_spendkey->setText("");
@ -111,6 +118,12 @@ void PageWalletRestoreKeys::showInputLines() {
bool PageWalletRestoreKeys::validatePage() { bool PageWalletRestoreKeys::validatePage() {
auto errStyle = "QLineEdit{border: 1px solid red;}"; 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_address->setStyleSheet("");
ui->line_viewkey->setStyleSheet(""); ui->line_viewkey->setStyleSheet("");
ui->label_errorString->hide(); ui->label_errorString->hide();

View file

@ -23,7 +23,8 @@ class PageWalletRestoreKeys : public QWizardPage
enum walletType { enum walletType {
ViewOnly = 0, ViewOnly = 0,
Spendable = 1, Spendable = 1,
Spendable_Nondeterministic = 2 Spendable_Nondeterministic = 2,
Multisig = 3,
}; };
public: public:

View file

@ -58,105 +58,167 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QFrame" name="frame_spendKey"> <widget class="QStackedWidget" name="stackedWidget">
<property name="frameShape"> <property name="currentIndex">
<enum>QFrame::NoFrame</enum> <number>0</number>
</property> </property>
<property name="frameShadow"> <widget class="QWidget" name="page">
<enum>QFrame::Raised</enum> <layout class="QVBoxLayout" name="verticalLayout_3">
</property> <property name="leftMargin">
<layout class="QVBoxLayout" name="verticalLayout_6"> <number>0</number>
<property name="leftMargin"> </property>
<number>0</number> <property name="topMargin">
</property> <number>0</number>
<property name="topMargin"> </property>
<number>0</number> <property name="rightMargin">
</property> <number>0</number>
<property name="rightMargin"> </property>
<number>0</number> <property name="bottomMargin">
</property> <number>0</number>
<property name="bottomMargin"> </property>
<number>0</number> <item>
</property> <widget class="QFrame" name="frame_spendKey">
<item> <property name="frameShape">
<widget class="QLabel" name="label_3"> <enum>QFrame::NoFrame</enum>
<property name="text"> </property>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Secret &lt;span style=&quot; font-weight:600;&quot;&gt;spend&lt;/span&gt; key&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <property name="frameShadow">
</property> <enum>QFrame::Raised</enum>
</widget> </property>
</item> <layout class="QVBoxLayout" name="verticalLayout_6">
<item> <property name="leftMargin">
<widget class="QLineEdit" name="line_spendkey"/> <number>0</number>
</item> </property>
</layout> <property name="topMargin">
</widget> <number>0</number>
</item> </property>
<item> <property name="rightMargin">
<widget class="QFrame" name="frame_viewKey"> <number>0</number>
<property name="frameShape"> </property>
<enum>QFrame::NoFrame</enum> <property name="bottomMargin">
</property> <number>0</number>
<property name="frameShadow"> </property>
<enum>QFrame::Raised</enum> <item>
</property> <widget class="QLabel" name="label_3">
<layout class="QVBoxLayout" name="verticalLayout_5"> <property name="text">
<property name="leftMargin"> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Secret &lt;span style=&quot; font-weight:600;&quot;&gt;spend&lt;/span&gt; key&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<number>0</number> </property>
</property> </widget>
<property name="topMargin"> </item>
<number>0</number> <item>
</property> <widget class="QLineEdit" name="line_spendkey"/>
<property name="rightMargin"> </item>
<number>0</number> </layout>
</property> </widget>
<property name="bottomMargin"> </item>
<number>0</number> <item>
</property> <widget class="QFrame" name="frame_viewKey">
<item> <property name="frameShape">
<widget class="QLabel" name="label_viewKey"> <enum>QFrame::NoFrame</enum>
<property name="text"> </property>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Secret &lt;span style=&quot; font-weight:600;&quot;&gt;view&lt;/span&gt; key&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <property name="frameShadow">
</property> <enum>QFrame::Raised</enum>
</widget> </property>
</item> <layout class="QVBoxLayout" name="verticalLayout_5">
<item> <property name="leftMargin">
<widget class="QLineEdit" name="line_viewkey"/> <number>0</number>
</item> </property>
</layout> <property name="topMargin">
</widget> <number>0</number>
</item> </property>
<item> <property name="rightMargin">
<widget class="QFrame" name="frame_address"> <number>0</number>
<property name="frameShape"> </property>
<enum>QFrame::NoFrame</enum> <property name="bottomMargin">
</property> <number>0</number>
<property name="frameShadow"> </property>
<enum>QFrame::Raised</enum> <item>
</property> <widget class="QLabel" name="label_viewKey">
<layout class="QVBoxLayout" name="verticalLayout_4"> <property name="text">
<property name="leftMargin"> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Secret &lt;span style=&quot; font-weight:600;&quot;&gt;view&lt;/span&gt; key&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<number>0</number> </property>
</property> </widget>
<property name="topMargin"> </item>
<number>0</number> <item>
</property> <widget class="QLineEdit" name="line_viewkey"/>
<property name="rightMargin"> </item>
<number>0</number> </layout>
</property> </widget>
<property name="bottomMargin"> </item>
<number>0</number> <item>
</property> <widget class="QFrame" name="frame_address">
<item> <property name="frameShape">
<widget class="QLabel" name="label"> <enum>QFrame::NoFrame</enum>
<property name="text"> </property>
<string>Primary address</string> <property name="frameShadow">
</property> <enum>QFrame::Raised</enum>
</widget> </property>
</item> <layout class="QVBoxLayout" name="verticalLayout_4">
<item> <property name="leftMargin">
<widget class="QLineEdit" name="line_address"/> <number>0</number>
</item> </property>
</layout> <property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Primary address</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="line_address"/>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_multisig">
<layout class="QVBoxLayout" name="verticalLayout_7">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Multisig seed:</string>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="multisigSeed"/>
</item>
</layout>
</widget>
</widget> </widget>
</item> </item>
<item> <item>

View file

@ -19,17 +19,32 @@
#include "PageHardwareDevice.h" #include "PageHardwareDevice.h"
#include "PageNetworkProxy.h" #include "PageNetworkProxy.h"
#include "PageNetworkWebsocket.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 "constants.h"
#include "WindowManager.h" #include "WindowManager.h"
#include "PageRecoverWallet.h"
#include <QLineEdit> #include "PageKeyType.h"
#include <QVBoxLayout>
#include <QScreen>
WalletWizard::WalletWizard(QWidget *parent) WalletWizard::WalletWizard(QWidget *parent)
: QWizard(parent) : QWizard(parent)
{ {
this->setWindowTitle("Welcome to Feather Wallet"); this->setWindowTitle("Feather Wizard");
this->setWindowIcon(QIcon(":/assets/images/appicons/64x64.png")); this->setWindowIcon(QIcon(":/assets/images/appicons/64x64.png"));
m_walletKeysFilesModel = new WalletKeysFilesModel(this); m_walletKeysFilesModel = new WalletKeysFilesModel(this);
@ -45,6 +60,9 @@ WalletWizard::WalletWizard(QWidget *parent)
auto walletSetPasswordPage = new PageSetPassword(&m_wizardFields, this); auto walletSetPasswordPage = new PageSetPassword(&m_wizardFields, this);
auto walletSetSeedPassphrasePage = new PageSetSeedPassphrase(&m_wizardFields, this); auto walletSetSeedPassphrasePage = new PageSetSeedPassphrase(&m_wizardFields, this);
auto walletSetSubaddressLookaheadPage = new PageSetSubaddressLookahead(&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_Menu, menuPage);
setPage(Page_WalletFile, createWallet); setPage(Page_WalletFile, createWallet);
setPage(Page_OpenWallet, openWalletPage); setPage(Page_OpenWallet, openWalletPage);
@ -60,6 +78,25 @@ WalletWizard::WalletWizard(QWidget *parent)
setPage(Page_SetSeedPassphrase, walletSetSeedPassphrasePage); setPage(Page_SetSeedPassphrase, walletSetSeedPassphrasePage);
setPage(Page_SetSubaddressLookahead, walletSetSubaddressLookaheadPage); setPage(Page_SetSubaddressLookahead, walletSetSubaddressLookaheadPage);
setPage(Page_Plugins, new PagePlugins(this)); 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); setStartId(Page_Menu);
@ -80,6 +117,8 @@ WalletWizard::WalletWizard(QWidget *parent)
layout << QWizard::CommitButton; layout << QWizard::CommitButton;
this->setButtonLayout(layout); this->setButtonLayout(layout);
this->setButtonText(QWizard::CommitButton, "Next");
auto *settingsButton = new QPushButton("Settings", this); auto *settingsButton = new QPushButton("Settings", this);
this->setButton(QWizard::CustomButton1, settingsButton); this->setButton(QWizard::CustomButton1, settingsButton);
@ -102,6 +141,11 @@ WalletWizard::WalletWizard(QWidget *parent)
emit openWallet(path, ""); emit openWallet(path, "");
}); });
connect(multisigRecoveryInfo, &PageMultisigMMSRecoveryInfo::showWallet, [this](Wallet* wallet){
m_wizardFields.wallet = nullptr;
emit showWallet(wallet);
});
connect(this, &QWizard::helpRequested, this, &WalletWizard::showHelp); connect(this, &QWizard::helpRequested, this, &WalletWizard::showHelp);
} }
@ -109,6 +153,10 @@ void WalletWizard::resetFields() {
m_wizardFields = {}; m_wizardFields = {};
} }
void WalletWizard::setWallet(Wallet *wallet) {
m_wizardFields.wallet = wallet;
}
void WalletWizard::onCreateWallet() { void WalletWizard::onCreateWallet() {
auto walletPath = QString("%1/%2").arg(m_wizardFields.walletDir, m_wizardFields.walletName); auto walletPath = QString("%1/%2").arg(m_wizardFields.walletDir, m_wizardFields.walletName);
@ -147,6 +195,21 @@ void WalletWizard::onCreateWallet() {
return; 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 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) { if (m_wizardFields.mode == WizardMode::CreateWallet && currentBlockHeight > 0) {
qInfo() << "New wallet, setting restore height to latest blockheight: " << currentBlockHeight; qInfo() << "New wallet, setting restore height to latest blockheight: " << currentBlockHeight;
@ -157,9 +220,10 @@ void WalletWizard::onCreateWallet() {
m_wizardFields.seed.setRestoreHeight(m_wizardFields.restoreHeight); 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() { QString WalletWizard::helpPage() {
@ -196,3 +260,7 @@ void WalletWizard::showHelp() {
windowManager()->showDocs(this, doc); windowManager()->showDocs(this, doc);
} }
} }
WalletWizard::~WalletWizard() {
delete m_wizardFields.wallet;
}

View file

@ -19,7 +19,9 @@ enum WizardMode {
OpenWallet, OpenWallet,
RestoreFromSeed, RestoreFromSeed,
RestoreFromKeys, RestoreFromKeys,
CreateWalletFromDevice CreateWalletFromDevice,
CreateMultisig,
RestoreMultisig
}; };
enum DeviceType { enum DeviceType {
@ -27,6 +29,18 @@ enum DeviceType {
TREZOR TREZOR
}; };
enum SignerConfig {
AUTOMATIC = 0,
SEMI_AUTOMATIC,
MANUAL
};
struct MMSSigner {
quint32 index = 0;
QString label;
QString address;
};
struct WizardFields { struct WizardFields {
QString walletName; QString walletName;
QString walletDir; QString walletDir;
@ -46,6 +60,17 @@ struct WizardFields {
Seed::Type seedType; Seed::Type seedType;
DeviceType deviceType; DeviceType deviceType;
QString subaddressLookahead; 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() { void clearFields() {
showSetSeedPassphrasePage = false; showSetSeedPassphrasePage = false;
@ -58,6 +83,7 @@ struct WizardFields {
secretSpendKey = ""; secretSpendKey = "";
restoreHeight = 0; restoreHeight = 0;
subaddressLookahead = ""; subaddressLookahead = "";
wallet = nullptr;
} }
WizardFields(): deviceType(DeviceType::LEDGER), mode(WizardMode::CreateWallet), WizardFields(): deviceType(DeviceType::LEDGER), mode(WizardMode::CreateWallet),
@ -78,26 +104,50 @@ public:
Page_SetSubaddressLookahead, Page_SetSubaddressLookahead,
Page_OpenWallet, Page_OpenWallet,
Page_Network, Page_Network,
Page_Recover,
Page_WalletRestoreSeed, Page_WalletRestoreSeed,
Page_WalletRestoreKeys, Page_WalletRestoreKeys,
Page_SetRestoreHeight, Page_SetRestoreHeight,
Page_HardwareDevice, Page_HardwareDevice,
Page_NetworkProxy, Page_NetworkProxy,
Page_NetworkWebsocket, 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); explicit WalletWizard(QWidget *parent = nullptr);
~WalletWizard() override;
void resetFields(); void resetFields();
void setWallet(Wallet* wallet);
signals: signals:
void initialNetworkConfigured(); void initialNetworkConfigured();
void showSettings(); void showSettings();
void openWallet(QString path, QString password); 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 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 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: private slots:
void onCreateWallet(); void onCreateWallet();

View file

@ -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 <QFileDialog>
#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;
}

View file

@ -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 <QWizardPage>
#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

View file

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PageMultisigCreateSetupKey</class>
<widget class="QWizardPage" name="PageMultisigCreateSetupKey">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>603</width>
<height>536</height>
</rect>
</property>
<property name="windowTitle">
<string>WizardPage</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="InfoFrame" name="infoFrame">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QRadioButton" name="radio_haveSetupKey">
<property name="text">
<string>I have a setup key</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radio_createSetupKey">
<property name="text">
<string>Create a new setup key</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>InfoFrame</class>
<extends>QFrame</extends>
<header>components.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View file

@ -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 <QFileDialog>
#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;
}

View file

@ -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 <QLabel>
#include <QWizardPage>
#include <QWidget>
#include <QDir>
#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

View file

@ -0,0 +1,147 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PageMultisigEnterChannel</class>
<widget class="QWizardPage" name="PageMultisigEnterChannel">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>561</width>
<height>310</height>
</rect>
</property>
<property name="windowTitle">
<string>WizardPage</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="InfoFrame" name="infoFrame">
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Messaging service:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="line_service">
<property name="text">
<string>http://ms.featherwallet.net:80/</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Policy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>5</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="check_authRequired">
<property name="text">
<string>This service requires authentication</string>
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="frame_auth">
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_key">
<property name="text">
<string>API key:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="line_apiKey"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="frame_confirm">
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Is this ok?</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radio_no">
<property name="text">
<string>No</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radio_yes">
<property name="text">
<string>Yes</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>InfoFrame</class>
<extends>QFrame</extends>
<header>components.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View file

@ -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;
}

View file

@ -0,0 +1,30 @@
// SPDX-License-Identifier: BSD-3-Clause
// SPDX-FileCopyrightText: 2020-2024 The Monero Project
#ifndef PAGEMULTISIGENTERNAME_H
#define PAGEMULTISIGENTERNAME_H
#include <QWizardPage>
#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

View file

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PageMultisigEnterName</class>
<widget class="QWizardPage" name="PageMultisigEnterName">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>646</width>
<height>477</height>
</rect>
</property>
<property name="windowTitle">
<string>WizardPage</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="InfoFrame" name="infoFrame">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Your name: </string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="line_name"/>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>InfoFrame</class>
<extends>QFrame</extends>
<header>components.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View file

@ -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;
}

View file

@ -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 <QWizardPage>
#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

View file

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PageMultisigEnterSetupKey</class>
<widget class="QWizardPage" name="PageMultisigEnterSetupKey">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>664</width>
<height>510</height>
</rect>
</property>
<property name="windowTitle">
<string>WizardPage</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="InfoFrame" name="infoFrame">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Setup key:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="line_setupKey"/>
</item>
<item>
<widget class="QFrame" name="frame_invalid">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Invalid setup key entered.</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="frame_verify">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label_verify">
<property name="text">
<string>You are about to create a n-of-m multisig wallet. Is that correct?</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radio_no">
<property name="text">
<string>No</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radio_yes">
<property name="text">
<string>Yes</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>InfoFrame</class>
<extends>QFrame</extends>
<header>components.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View file

@ -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 <QFileDialog>
#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.<br><br>"
"It might be fundamentally broken, which could result in a <u>loss of funds</u>.<br><br>"
"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;
}

View file

@ -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 <QWizardPage>
#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

View file

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PageMultisigExperimentalWarning</class>
<widget class="QWizardPage" name="PageMultisigExperimentalWarning">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>497</width>
<height>330</height>
</rect>
</property>
<property name="windowTitle">
<string>WizardPage</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="InfoFrame" name="warningFrame">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label_warning">
<property name="text">
<string>TextLabel</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="check_confirm">
<property name="text">
<string>I understand.</string>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>InfoFrame</class>
<extends>QFrame</extends>
<header>components.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View file

@ -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();
}

View file

@ -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 <QLabel>
#include <QWizardPage>
#include <QWidget>
#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

View file

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PageMultisigMMSRecoveryInfo</class>
<widget class="QWizardPage" name="PageMultisigMMSRecoveryInfo">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>648</width>
<height>570</height>
</rect>
</property>
<property name="windowTitle">
<string>WizardPage</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="InfoFrame" name="infoFrame">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="mmsRecoveryInfo"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="btn_saveToFile">
<property name="text">
<string>Save to file</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_copy">
<property name="text">
<string>Copy</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="check_saved">
<property name="text">
<string>I have saved my multisig seed and MMS recovery info.</string>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>InfoFrame</class>
<extends>QFrame</extends>
<header>components.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View file

@ -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;
}

Some files were not shown because too many files have changed in this diff Show more