From 495d6b49c881ccd8479270b7df7795707cc3f230 Mon Sep 17 00:00:00 2001 From: levoncrypto Date: Thu, 11 Dec 2025 12:32:53 +0400 Subject: [PATCH] UI modernization and new Spark Assets UI --- src/Makefile.qt.include | 4 + src/qt/CMakeLists.txt | 2 + src/qt/bitcoingui.cpp | 57 +- src/qt/bitcoingui.h | 2 + src/qt/forms/overviewpage.ui | 1368 ++++++++++++++-------------- src/qt/forms/receivecoinsdialog.ui | 99 ++ src/qt/forms/sendcoinsdialog.ui | 77 ++ src/qt/forms/sparkassetspage.ui | 504 ++++++++++ src/qt/overviewpage.cpp | 713 ++++++++------- src/qt/overviewpage.h | 14 +- src/qt/receivecoinsdialog.cpp | 22 + src/qt/receivecoinsdialog.h | 1 + src/qt/res/css/firo.css | 212 +++++ src/qt/sendcoinsdialog.cpp | 30 +- src/qt/sendcoinsdialog.h | 1 + src/qt/sendcoinsentry.cpp | 26 +- src/qt/sparkassetspage.cpp | 952 +++++++++++++++++++ src/qt/sparkassetspage.h | 78 ++ src/qt/transactionrecord.cpp | 38 + src/qt/transactionrecord.h | 6 +- src/qt/transactionview.cpp | 258 ++++-- src/qt/transactionview.h | 4 +- src/qt/walletframe.cpp | 8 + src/qt/walletframe.h | 1 + src/qt/walletview.cpp | 20 +- src/qt/walletview.h | 9 +- src/spats/registry.hpp | 3 + 27 files changed, 3390 insertions(+), 1119 deletions(-) create mode 100644 src/qt/forms/sparkassetspage.ui create mode 100644 src/qt/sparkassetspage.cpp create mode 100644 src/qt/sparkassetspage.h diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index c67c96bccf..3b49938201 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -106,6 +106,7 @@ QT_FORMS_UI = \ qt/forms/helpmessagedialog.ui \ qt/forms/masternodelist.ui \ qt/forms/myownspats.ui \ + qt/forms/sparkassetspage.ui \ qt/forms/sparkassetdialog.ui \ qt/forms/spatsburndialog.ui \ qt/forms/spatsmintdialog.ui \ @@ -151,6 +152,7 @@ QT_MOC_CPP = \ qt/moc_macnotificationhandler.cpp \ qt/moc_masternodelist.cpp \ qt/moc_myownspats.cpp \ + qt/moc_sparkassetspage.cpp \ qt/moc_sparkassetdialog.cpp \ qt/moc_spatsburndialog.cpp \ qt/moc_spatsmintdialog.cpp \ @@ -243,6 +245,7 @@ BITCOIN_QT_H = \ qt/optionsdialog.h \ qt/optionsmodel.h \ qt/overviewpage.h \ + qt/sparkassetspage.h \ qt/paymentserver.h \ qt/peertablemodel.h \ qt/platformstyle.h \ @@ -442,6 +445,7 @@ BITCOIN_QT_WALLET_CPP = \ qt/manualmintdialog.cpp \ qt/openuridialog.cpp \ qt/overviewpage.cpp \ + qt/sparkassetspage.cpp \ qt/paymentserver.cpp \ qt/receivecoinsdialog.cpp \ qt/receiverequestdialog.cpp \ diff --git a/src/qt/CMakeLists.txt b/src/qt/CMakeLists.txt index 3b4a1d3e25..a7ffbb50ec 100644 --- a/src/qt/CMakeLists.txt +++ b/src/qt/CMakeLists.txt @@ -85,6 +85,7 @@ add_library(firoqt STATIC EXCLUDE_FROM_ALL optionsdialog.cpp optionsmodel.cpp overviewpage.cpp + sparkassetspage.cpp paymentserver.cpp peertablemodel.cpp platformstyle.cpp @@ -185,6 +186,7 @@ if(ENABLE_WALLET) myownspats.cpp openuridialog.cpp overviewpage.cpp + sparkassetspage.cpp paymentserver.cpp receivecoinsdialog.cpp receiverequestdialog.cpp diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index 4c622718c2..6674ad26ea 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -126,6 +126,7 @@ BitcoinGUI::BitcoinGUI(const PlatformStyle *_platformStyle, const NetworkStyle * lelantusAction(0), masternodeAction(0), myownspatsAction(nullptr), + sparkAssetsAction(nullptr), logoAction(0), trayIcon(0), trayIconMenu(0), @@ -376,6 +377,13 @@ void BitcoinGUI::createActions() myownspatsAction->setShortcut(QKeySequence(Qt::ALT + key++)); tabGroup->addAction(myownspatsAction); + sparkAssetsAction = new QAction(tr("&Spark Assets"), this); + sparkAssetsAction->setStatusTip(tr("Manage and view your Spark assets")); + sparkAssetsAction->setToolTip(sparkAssetsAction->statusTip()); + sparkAssetsAction->setCheckable(true); + sparkAssetsAction->setShortcut(QKeySequence(Qt::ALT + key++)); + tabGroup->addAction(sparkAssetsAction); + #ifdef ENABLE_WALLET connect(masternodeAction, &QAction::triggered, [this]{ showNormalIfMinimized(); }); connect(masternodeAction, &QAction::triggered, this, &BitcoinGUI::gotoMasternodePage); @@ -393,6 +401,8 @@ void BitcoinGUI::createActions() connect(receiveCoinsMenuAction, &QAction::triggered, this, &BitcoinGUI::gotoReceiveCoinsPage); connect(historyAction, &QAction::triggered, this, [this]{ showNormalIfMinimized(); }); connect(historyAction, &QAction::triggered, this, &BitcoinGUI::gotoHistoryPage); + connect(sparkAssetsAction, &QAction::triggered, [this]{ showNormalIfMinimized(); }); + connect(sparkAssetsAction, &QAction::triggered, this, &BitcoinGUI::gotoSparkAssetsPage); #endif // ENABLE_WALLET @@ -529,25 +539,57 @@ void BitcoinGUI::createToolBars() toolbar = addToolBar(tr("Tabs toolbar")); toolbar->setContextMenuPolicy(Qt::PreventContextMenu); toolbar->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - toolbar->setToolButtonStyle(Qt::ToolButtonTextOnly); toolbar->setMovable(false); - toolbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + toolbar->setToolButtonStyle(Qt::ToolButtonTextOnly); toolbar->addAction(overviewAction); toolbar->addAction(sendCoinsAction); toolbar->addAction(receiveCoinsAction); toolbar->addAction(historyAction); - toolbar->addAction(lelantusAction); toolbar->addAction(masternodeAction); toolbar->addAction(myownspatsAction); + toolbar->addAction(sparkAssetsAction); logoLabel = new QLabel(); logoLabel->setObjectName("lblToolbarLogo"); logoLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - toolbar->addWidget(logoLabel); overviewAction->setChecked(true); } + + toolbar->setStyleSheet(R"( + QToolBar { + background: #F7F8FA; + border: none; + spacing: 8px; + padding: 4px 8px; + } + + QToolButton { + background-color: #F7F8FA; + color: #111827; + border: none; + border-radius: 9px; + min-width: 90px; + min-height: 28px; + font-size: 10pt; + font-weight: 800; + padding: 2px 8px; + margin: 1px 2px; + } + + QToolButton:hover { + background-color: #E5E7EB; + } + + QToolButton:checked { + background-color: #B24040; + color: #FFFFFF; + border-radius: 9px; + padding: 2px 8px; + } + )"); + } void BitcoinGUI::setClientModel(ClientModel *_clientModel) @@ -824,6 +866,13 @@ void BitcoinGUI::gotoSendCoinsPage(QString addr) if (walletFrame) walletFrame->gotoSendCoinsPage(addr); } +void BitcoinGUI::gotoSparkAssetsPage() +{ + if (!walletFrame) + return; + walletFrame->gotoSparkAssetsPage(); +} + void BitcoinGUI::gotoSignMessageTab(QString addr) { if (walletFrame) walletFrame->gotoSignMessageTab(addr); diff --git a/src/qt/bitcoingui.h b/src/qt/bitcoingui.h index a13574fc6a..9d030fec30 100644 --- a/src/qt/bitcoingui.h +++ b/src/qt/bitcoingui.h @@ -125,6 +125,7 @@ class BitcoinGUI : public QMainWindow QAction *lelantusAction; QAction *masternodeAction; QAction *myownspatsAction; + QAction *sparkAssetsAction; QAction *logoAction; QToolBar *toolbar; QLabel *logoLabel; @@ -227,6 +228,7 @@ public Q_SLOTS: void gotoReceiveCoinsPage(); /** Switch to send coins page */ void gotoSendCoinsPage(QString addr = ""); + void gotoSparkAssetsPage(); /** Show Sign/Verify Message dialog and switch to sign message tab */ void gotoSignMessageTab(QString addr = ""); diff --git a/src/qt/forms/overviewpage.ui b/src/qt/forms/overviewpage.ui index 8a68f21311..82b43639c9 100644 --- a/src/qt/forms/overviewpage.ui +++ b/src/qt/forms/overviewpage.ui @@ -6,746 +6,760 @@ 0 0 - 769 - 407 + 1040 + 680 - Form + Overview + 16 - - font-size: 14px; color: #92400E; background-color:#FEF3C7; - - - QFrame::StyledPanel - - - QFrame::Raised - - + QFrame::StyledPanel + QFrame::Raised + - - - Qt::Horizontal - - - - 20 - 20 - - - - - - - - - - - Qt::AlignCenter - - - - - - - padding-left: -60px; padding-right: -60px; font-weight: bold - - - Click here - - + + + + + We have detected Lelantus coins that have not been migrated to Spark… + + + color:#92400E; font-size:13px; + + true + + + + + Migrate + + + - - - - - Qt::AlignCenter + to migrate … + + color:#92400E; font-size:13px; - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - false - - - QLabel { background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop:0 #F0D0A0, stop:1 #F8D488); color:#000000; } - - - true - - - 3 - - - Qt::TextSelectableByMouse - + + false + true - - - - - - - QFrame::StyledPanel - - - QFrame::Raised - - + + 16 + 16 + + + + 1 + + + FIRO (Primary) + Qt::AlignLeft|Qt::AlignVCenter + + + QLabel { + background: transparent; + color: #6B7280; + font-size: 18pt; + font-weight: 700; + } + + + + + + - - - - - - 75 - true - - - - Private Balances - - - - - - - true - - - - 30 - 16777215 - - - - The displayed information may be out of date. Your wallet automatically synchronizes with the Firo network after a connection is established, but this process has not completed yet. - - - - - - - :/icons/warning - :/icons/warning:/icons/warning - - - - 24 - 24 - - - - true - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - + + Total: + Qt::AlignLeft|Qt::AlignVCenter + - - - QAbstractItemView::NoEditTriggers - - - true - - - QAbstractItemView::SingleSelection - - - QAbstractItemView::SelectRows - - - true - - - 9 - - - true - - - - Asset ID - - - - - NFT ID - - - - - Name - - - - - Symbol - - - - - Available - - - - - Pending - - - - - Fungible - - - - - Metadata - - - - - Description - - + + 0.00 FIRO + Qt::AlignLeft|Qt::AlignVCenter - - - 12 - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 75 - true - - - - IBeamCursor - - - Total of transactions that have yet to be confirmed, and do not yet count toward the spendable balance - - - 0.000 000 00 FIRO - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Immature: - - - - - - - - 75 - true - - - - IBeamCursor - - - Your current total balance - - - 0.000 000 00 FIRO - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Pending: - - + + Qt::Horizontal + 4020 + + + + + + + 2 + + + + 1 + + + Private Available: + color:#6B7280; font-size:13px; + - - - - Watch-only: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - + + + 0.00 FIRO + color:#111827; font-weight:600; font-size:13px; + - - - - - 0 - 0 - - - - - 10 - - - - Anonymize All - - + + + + + + + 1 + + + Private Pending: + color:#6B7280; font-size:13px; + - - - - Available: - - + + + 0.00 FIRO + color:#111827; font-weight:600; font-size:13px; + - - - - - 75 - true - - - - Transparent Balances - - + + + + + + + 1 + + + Transparent Available: + color:#6B7280; font-size:13px; + - - - - - 75 - true - - - - IBeamCursor - - - Unconfirmed transactions to watch-only addresses - - - 0.000 000 00 FIRO - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - + + + 0.00 FIRO + color:#111827; font-weight:600; font-size:13px; + - - - - - 75 - true - - - - 0.000 000 00 FIRO - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - + + + + + + + 1 + + + Transparent Pending: + color:#6B7280; font-size:13px; + - - - - - 75 - true - - - - IBeamCursor - - - Current total balance in watch-only addresses - - - 0.000 000 00 FIRO - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - + + + 0.00 FIRO + color:#111827; font-weight:600; font-size:13px; + - - - - Qt::Horizontal - - + + + + + + + 1 + + + Transparent Immature: + color:#6B7280; font-size:13px; + - - - - - 75 - true - - - - IBeamCursor - - - Mined balance that has not yet matured - - - 0.000 000 00 FIRO - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - + + + 0.00 FIRO + color:#111827; font-weight:600; font-size:13px; + - - - - Spendable: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - + + + + + + + + 6 + + + Spendable: + color:#6B7280; font-size:12px; + - - - - - 75 - true - - - - IBeamCursor - - - Mined balance in watch-only addresses that has not yet matured - - - 0.000 000 00 FIRO - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - + + + Watch-only: + color:#6B7280; font-size:12px; + - - - - - 75 - true - - - - IBeamCursor - - - Your current balance in watch-only addresses - - - 0.000 000 00 FIRO - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - + + + 0.00 FIRO + color:#111827; font-weight:600; font-size:12px; + - - - - Anonymizable: - - + + + 0.00 FIRO + color:#111827; font-weight:600; font-size:12px; + - - - - - 0 - 0 - - - - - 140 - 0 - - - - Qt::Horizontal - - + + + 0.00 FIRO + color:#111827; font-weight:600; font-size:12px; + - - - - Total: - - + + + Qt::Horizontal + - - - - - 75 - true - - - - IBeamCursor - - - Your current spendable balance - - - 0.000 000 00 FIRO - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - + + + 0.00 FIRO + color:#111827; font-weight:700; font-size:12px; + - + + + + + 10 + + + Anonymize All + + + + + secondaryButton + Send + + + + + secondaryButton + Receive + + + + + Qt::Horizontal + 4020 + - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - true - - - Anonymous communication with Tor - - - false - - - - + + + - - - - - - QFrame::StyledPanel - - - QFrame::Raised - - + + + + + + 8 + + + Spark Assets + + + QLabel#labelSparkAssets { + background: transparent; + color: #111111; + font-size: 18pt; + font-weight: 700; + } + + + + + + + Qt::Horizontal + + - + + Search asset... + + + QLineEdit { + border: 1px solid #D1D5DB; + border-radius: 8px; + padding: 6px 10px; + font-size: 12pt; + background-color: #FFFFFF; + } + QLineEdit:focus { + border: 1px solid #CC5B5B; + outline: none; + } + + + + + + + + + 8 - - - - 75 - true - - - - Recent transactions - - + + true + true + Spark Assets + + + QPushButton { + background-color: #B24040; + color: #FFFFFF; + border: none; + border-radius: 10px; + padding: 4px 14px; + font-weight: 600; + font-size: 9pt; + } + QPushButton:hover { + background-color: #993333; + } + QPushButton:checked { + background-color: #B24040; + color: #FFFFFF; + } + + + - - - true - - - - 30 - 16777215 - - - - The displayed information may be out of date. Your wallet automatically synchronizes with the Firo network after a connection is established, but this process has not completed yet. - - - - - - - :/icons/warning - :/icons/warning:/icons/warning - - - - 24 - 24 - - - - true - - + + true + NFT + + + QPushButton { + background-color: #E5E7EB; + color: #111827; + border: none; + border-radius: 10px; + padding: 4px 14px; + font-weight: 600; + font-size: 9pt; + } + QPushButton:hover { + background-color: #D1D5DB; + } + QPushButton:checked { + background-color: #B24040; + color: #FFFFFF; + } + + + - - - Qt::Horizontal - - - - 40 - 20 - - - + + Qt::Horizontal + - - - - + + + + + + 10 + + + + + Qt::Horizontal + + + + 60 + 10 + + + + + + + + Asset ID + + + Qt::AlignLeft|Qt::AlignVCenter + + + 120 + - QListView { background: transparent; } + + color:#6B7280; + font-size:10pt; + font-weight:600; + background:transparent; + - - QFrame::NoFrame + + + + + + Name - - Qt::ScrollBarAlwaysOff + + Qt::AlignLeft|Qt::AlignVCenter - - Qt::ScrollBarAlwaysOff + + 220 - - QAbstractItemView::NoSelection + + + color:#6B7280; + font-size:10pt; + font-weight:600; + background:transparent; + - + + + + + + Qt::Horizontal + + + + 40 + 10 + + + + + + + + Available + + + Qt::AlignRight|Qt::AlignVCenter + + + 120 + + + + color:#6B7280; + font-size:10pt; + font-weight:600; + background:transparent; + + + + + + + + Qt::Horizontal + + + + 40 + 10 + + + + + + + + + + + 4 + + + true + QFrame::NoFrame + + + QScrollArea { + background: transparent; + border: none; + } + QScrollArea > QWidget > QWidget { + background: #FFFFFF; + border: none; + } + QWidget#scrollWidgetSparkAssets { + background: #FFFFFF; + border: none; + } + + + + + 4 + + + + + + + + + + + + No NFTs yet. Create or import your first NFT. + + + Qt::AlignCenter + + + + color:#6B7280; font-size:12pt; + + + + + + + + + + + 0 + + + Qt::Horizontal + - + + + View all + + PointingHandCursor + + + + QPushButton { + background: transparent; + border: none; + color: #B24040; + font-size: 10pt; + font-weight: 600; + text-decoration: none; + padding: 0; + margin-right: 12px; + } + QPushButton:hover { + color: #993333; + text-decoration: underline; + } + + + + + + + + + 10 + + + Qt::Horizontal + 4020 + + + + + + + + + + + + + 4 + + + Recent Activity + + + background: transparent; + color:#111827; + font-size:14px; + font-weight:700; + + + + + + + 6 + + + true + true + All + + + QPushButton { + background-color: #B24040; + color: #FFFFFF; + border: none; + border-radius: 10px; + font-size: 8pt; + font-weight: 600; + padding: 3px 10px; + } + QPushButton:hover { background-color: #993333; } + QPushButton:checked { + background-color: #B24040; + color: #FFFFFF; + } + + + + + + + true + FIRO + + + QPushButton { + background-color: #E5E7EB; + color: #111827; + border: none; + border-radius: 12px; + font-size: 8pt; + font-weight: 600; + padding: 3px 12px; + } + QPushButton:hover { background-color: #D1D5DB; } + QPushButton:checked { + background-color: #B24040; + color: #FFFFFF; + } + + + + + + + true + Assets + + + QPushButton { + background-color: #E5E7EB; + color: #111827; + border: none; + border-radius: 12px; + font-size: 8pt; + font-weight: 600; + padding: 3px 12px; + } + QPushButton:hover { background-color: #D1D5DB; } + QPushButton:checked { + background-color: #B24040; + color: #FFFFFF; + } + + + + + + + + + + + + + QListView, QListView::viewport { + background: #FFFFFF; + border: none; + } + + - - - - - Qt::Vertical - - - - 20 - 40 - - - - + + + + + + Qt::Horizontal + 4020 + + + + + Route via Tor + + + QCheckBox { + background: transparent; + color: #111827; + font-size: 10pt; + font-weight: 600; + padding: 3px 6px; + } + QCheckBox::indicator { + width: 16px; + height: 16px; + } + + + + + + + Qt::Horizontal + 20020 + + - + - - - - - - - + + + + + + + + diff --git a/src/qt/forms/receivecoinsdialog.ui b/src/qt/forms/receivecoinsdialog.ui index 96b067fe33..50782b8451 100644 --- a/src/qt/forms/receivecoinsdialog.ui +++ b/src/qt/forms/receivecoinsdialog.ui @@ -2,6 +2,105 @@ ReceiveCoinsDialog + + +QWidget#ReceiveCoinsDialog { + background: #F7F8FA; + font-family: "Segoe UI"; +} +QFrame#frame, QFrame#frame2 { + background: #FFFFFF; + border-radius: 18px; + border: 1px solid #F3F4F6; + box-shadow: + 0 8px 15px rgba(0,0,0,0.10), + 0 4px 6px rgba(0,0,0,0.06), + inset 0 1px 3px rgba(255,255,255,0.8); +} +QLabel { + color: #111827; + font-size: 11pt; +} + +QLineEdit, +BitcoinAmountField, +QComboBox { + background: #FFFFFF; + border-radius: 10px; + border: 1px solid #D1D5DB; + padding: 6px 10px; + font-size: 10pt; + color: #111827; +} + +QTableView { + background: #FFFFFF; + border-radius: 14px; + border: 1px solid #E5E7EB; + font-size: 10pt; + gridline-color: #FFFFFF; + alternate-background-color: transparent; + selection-background-color: #F1F3F5; +} + +QHeaderView::section { + background: #FFFFFF; + padding: 6px; + border: none; + font-weight: 600; + color: #6B7280; +} +QPushButton { + background: #F1F3F5; + border-radius: 10px; + border: 1px solid #E5E7EB; + padding: 6px 14px; + font-size: 10pt; + font-weight: 600; + color: #111827; +} + +QPushButton:hover { + background: #E5E7EB; +} + +QPushButton#receiveButton { + background: #B24040; + border: none; + color: #FFFFFF; + font-weight: 600; + border-radius: 12px; +} + +QPushButton#receiveButton:hover { + background: #B24040; +} + +QPushButton#createSparkNameButton { + background: #B24040; + border-radius: 12px; + border: 1px solid #D1D5DB; + padding: 6px 18px; + color: #FFFFFF; +} + +QPushButton#createSparkNameButton:hover { + background: #F3F4F6; +} + +QLabel { + background: transparent; + color: #111827; + font-size: 15pt; +} + +QFrame#frame QLabel, +QFrame#frame2 QLabel { + background: #FFFFFF; +} + + + 0 diff --git a/src/qt/forms/sendcoinsdialog.ui b/src/qt/forms/sendcoinsdialog.ui index 780e9e9b9f..3c3a3d56aa 100644 --- a/src/qt/forms/sendcoinsdialog.ui +++ b/src/qt/forms/sendcoinsdialog.ui @@ -2,6 +2,83 @@ SendCoinsDialog + + +QWidget#SendCoinsDialog { + background: #F7F8FA; + font-family: "Segoe UI"; +} + +QFrame { + background: #FFFFFF; + border-radius: 18px; + border: 1px solid #F3F4F6; +} + +QLabel { + color: #111827; + font-size: 15pt; +} + +QLineEdit, +QComboBox, +QValidatedLineEdit, +BitcoinAmountField { + background: #FFFFFF; + border-radius: 10px; + border: 1px solid #D1D5DB; + padding: 6px 10px; + font-size: 10pt; + color: #111827; +} + +QPushButton#sendButton { + background: #B24040; + border-radius: 12px; + font-weight: 600; + color: #FFFFFF; + padding: 8px 20px; +} +QPushButton#sendButton:hover { + background: #9F3434; +} + +QPushButton { + background: #B24040; + border-radius: 10px; + border: 1px solid #E5E7EB; + padding: 6px 14px; + font-size: 10pt; + color: #FFFFFF; +} +QPushButton:hover { + background: #E5E7EB; +} + +QTableView { + background: #FFF; + border-radius: 14px; + border: 1px solid #E5E7EB; + font-size: 10pt; + gridline-color: #FFFFFF; +} + +QHeaderView::section { + background: #FFFFFF; + padding: 6px; + border: none; + font-weight: 600; + color: #6B7280; +} + +QFrame#SendCoins { + background: #FFFFFF; + border-radius: 18px; + border: 1px solid #E5E7EB; + padding: 16px; +} + + 0 diff --git a/src/qt/forms/sparkassetspage.ui b/src/qt/forms/sparkassetspage.ui new file mode 100644 index 0000000000..ddfe38d7b2 --- /dev/null +++ b/src/qt/forms/sparkassetspage.ui @@ -0,0 +1,504 @@ + + + SparkAssetsPage + + + 001080680 + + + + QWidget#SparkAssetsPage { + background: #F7F8FA; + font-family: "Segoe UI"; + } + QPushButton { + background: #E5E7EB; + border-radius: 10px; + color: #111827; + border: 1px solid #D1D5DB; + } + QPushButton#btnPortfolio, + QPushButton#btnMyCreations, + QPushButton#btnCreateAsset { + background: #FFFFFF; + border: 1px solid #D1D5DB; + border-radius: 8px; + padding: 6px 20px; + color: #111827; + + box-shadow: + 0px 3px 6px rgba(0,0,0,0.08), + -1px -1px 4px rgba(255,255,255,0.9); + } + QPushButton#btnPortfolio:checked, + QPushButton#btnMyCreations:checked, + QPushButton#btnCreateAsset:checked { + background: #B24040; + border: 1px solid #B24040; + color: white; + } + + QLineEdit#searchAssets { + background: #FFFFFF; + border: 1px solid #D1D5DB; + border-radius: 10px; + padding: 6px 12px; + font-size: 10pt; + color: #111827; + } + + QTableWidget { + background: #FFFFFF; + border: none; + gridline-color: #FFFFFF; + selection-background-color: transparent; + selection-color: #111827; + alternate-background-color: transparent; + } + + QHeaderView::section { + background: #FFFFFF; + color: #6B7280; + font-size: 10pt; + font-weight: 600; + border: none; + padding-bottom: 6px; + } + + QTableWidget::item { + font-size: 20pt; + font-weight: 700; + color: #111827; + padding: 8px 0; + border: none; + background: #FFFFFF; + } + + QTableWidget::item:selected { + background: #F3F4F6; ; + color: #111827; + } + + QTextEdit { + background: #FFFFFF; + border: none; + font-size: 10pt; + color: #111827; + } + QFrame#assetsCard, + QFrame#detailsCard, + QFrame#frameMyCreated, + QFrame#frameActivity { + background: #FFFFFF; + border-radius: 18px; + border: 1px solid #F3F4F6; + box-shadow: + 0px 8px 15px rgba(0,0,0,0.10), + 0px 4px 6px rgba(0,0,0,0.06), + inset 0px 1px 3px rgba(255,255,255,0.8); + } + + QPushButton#btnMint, + QPushButton#btnBurn, + QPushButton#btnMetadata, + QPushButton#btnResupply, + QPushButton#btnRevoke, + QPushButton#btnSend, + QPushButton#btnReceive, + QPushButton#btnAddWatch, + QPushButton#btnRemove, + QPushButton#btnExport, + QPushButton#btnCopy, + QPushButton#btnDetailsSend, + QPushButton#btnDetailsReceive, + QPushButton#btnAll, + QPushButton#btnHeld, + QPushButton#btnWatchOnly, + QPushButton#btnRefresh { + background: #F1F3F5; + border-radius: 10px; + padding: 4px 10px; + font-size: 9pt; + font-weight: 600; + color: #111827; + border: 1px solid #E5E7EB; + box-shadow: + 2px 2px 6px rgba(0,0,0,0.08), + -2px -2px 6px rgba(255,255,255,0.8); + } + + + + + + + true + Portfolio + + + true + My Creations + + + true + Create Asset + + + Qt::Horizontal + + + + + + + 10 + + + Search assets, symbol, identifier... + + + + + Filter: + + color:#6B7280; font-size:10pt; + + + + All + Held + Watch-only + + Qt::Horizontal + + + Refresh + + + + + + + + + + + + 10 + + + Assets + + + font-weight:700; font-size:14pt; color:#111827; background:#FFFFFF; + + + + + + + Asset ID + Name + Available + + + + + Send + Receive + Add Watch Asset + Remove + Export... + + Qt::Horizontal + + + + + + + + + + 10 + + + Asset Details + + + font-weight:700; font-size:20pt; color:#111827; background: #FFFFFF; + + + + + + + true + + + background:#FFFFFF; border:none; font-size:10pt; color:#111827; + + + + + + + Send + Receive + Copy Identifier + + Qt::Horizontal + + + + + + + + + + + 15 + + + + + + My Created Assets + + + font-weight:700; font-size:14pt; color:#111827; background: #FFFFFF; + + + + + + + Asset ID + Name + Available + + + + + 8 + + + Mint... + + + + + Burn... + + + + + Change Metadata... + + + + + Resupply Settings... + + + + + Revoke / Retire... + + + + + Qt::Horizontal + + + + + + + + + + + + + Activity (selected asset) + + + font-weight:700; font-size:13pt; color:#111827; background: #FFFFFF; + + + + + + + Date + Type + Txid + Notes + + + + + + + + + + 20 + + + + + background:#FFFFFF; + border-radius:18px; + border:1px solid #F3F4F6; + padding:10px; + + + + + + + 25 + 15 + + + Create Spark Asset + + font-size:18pt; font-weight:700; + + + + + + + + + Essentials + font-weight:600; + + + + + Fungible asset + + + + + Asset Type: + + + + + true + Auto + + + + Name: + + + + + + Symbol: + + + + + + Total Supply: + + + + + + Precision: + + + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + + + + Resupplyable: + + + + Yes + No + + + + + + + + + + Advanced + font-weight:600; + + + + Identifier: + + + + + Auto + Generate + + + Description: + + + + + + + + Metadata (JSON / key-value) + + + + + + + + + + + + + + Create Asset + background:#B24040; color:white; + + + Clear + Estimate Fee ▾ + + Qt::Horizontal + + + + + + + + + + + + + diff --git a/src/qt/overviewpage.cpp b/src/qt/overviewpage.cpp index 8753b30550..d881a8725b 100644 --- a/src/qt/overviewpage.cpp +++ b/src/qt/overviewpage.cpp @@ -17,13 +17,13 @@ #include "platformstyle.h" #include "transactionfilterproxy.h" #include "transactiontablemodel.h" +#include "transactionrecord.h" #include "walletmodel.h" #include "validation.h" #include "askpassphrasedialog.h" #include "spatsburndialog.h" #include "spatsuserconfirmationdialog.h" - #ifdef WIN32 #include #endif @@ -34,6 +34,9 @@ #include #include #include +#include +#include +#include #include "../spark/state.h" @@ -52,11 +55,20 @@ enum SparkAssetColumns { ColumnFungible, ColumnMetadata, ColumnDescription, - ColumnCount // This keeps the count of total columns, always keep last! + ColumnCount }; } +static quint32 MakeTypeMask(std::initializer_list types) +{ + quint32 mask = 0; + for (auto t : types) { + mask |= TransactionFilterProxy::TYPE(static_cast(t)); + } + return mask; +} + class TxViewDelegate : public QAbstractItemDelegate { Q_OBJECT @@ -65,19 +77,23 @@ class TxViewDelegate : public QAbstractItemDelegate QAbstractItemDelegate(parent), unit(BitcoinUnits::BTC), platformStyle(_platformStyle) { - } inline void paint(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index ) const + const QModelIndex &index ) const override { painter->save(); + if (option.state & QStyle::State_Selected) { + painter->fillRect(option.rect, QColor("#FFFFFF")); + } else { + painter->fillRect(option.rect, QColor("#FFFFFF")); + } QIcon icon = qvariant_cast(index.data(TransactionTableModel::RawDecorationRole)); - QRect mainRect = option.rect; + QRect mainRect = option.rect.adjusted(6, 6, -6, -6); QRect decorationRect(mainRect.topLeft(), QSize(DECORATION_SIZE, DECORATION_SIZE)); - int xspace = DECORATION_SIZE + 8; - int ypad = 6; + int xspace = DECORATION_SIZE + 10; + int ypad = 4; int halfheight = (mainRect.height() - 2*ypad)/2; QRect amountRect(mainRect.left() + xspace, mainRect.top()+ypad, mainRect.width() - xspace, halfheight); QRect addressRect(mainRect.left() + xspace, mainRect.top()+ypad+halfheight, mainRect.width() - xspace, halfheight); @@ -133,9 +149,9 @@ class TxViewDelegate : public QAbstractItemDelegate painter->restore(); } - inline QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const + inline QSize sizeHint(const QStyleOptionViewItem&, const QModelIndex&) const override { - return QSize(DECORATION_SIZE, DECORATION_SIZE); + return QSize(DECORATION_SIZE, DECORATION_SIZE + 12); } int unit; @@ -144,6 +160,11 @@ class TxViewDelegate : public QAbstractItemDelegate }; #include "overviewpage.moc" +auto &getSpatsManager() +{ + return spark::CSparkState::GetState()->GetSpatsManager(); +} + OverviewPage::OverviewPage(const PlatformStyle *platformStyle, QWidget *parent) : QWidget(parent), ui(new Ui::OverviewPage), @@ -159,47 +180,161 @@ OverviewPage::OverviewPage(const PlatformStyle *platformStyle, QWidget *parent) { ui->setupUi(this); - // read config - bool torEnabled; - if(IsArgSet("-torsetup")){ - torEnabled = GetBoolArg("-torsetup", DEFAULT_TOR_SETUP); - }else{ - torEnabled = settings.value("fTorSetup").toBool(); - } + auto fixCapsule = [](QFrame* f){ + f->setFixedHeight(28); + f->setContentsMargins(8, 0, 8, 0); + }; + fixCapsule(ui->framePrivateAvailable); + fixCapsule(ui->framePrivatePending); + fixCapsule(ui->frameTransparentAvailable); + fixCapsule(ui->frameTransparentPending); + fixCapsule(ui->frameTransparentImmature); + + ui->labelPrimaryText->setStyleSheet(R"( QLabel { background: transparent; color:rgb(70, 75, 84); font-size: 18pt; font-weight: 700; } )"); + ui->sparkCard->setMaximumHeight(510); + ui->activityCard->setMaximumHeight(510); + ui->sparkCard->setStyleSheet(R"( + QFrame#sparkCard { + background: #FFFFFF; + border-radius: 15px; + border: 1px solidrgb(151, 149, 149); + padding: 8px; + } + )"); + + ui->activityCard->setStyleSheet(R"( + QFrame#activityCard { + background: #FFFFFF; + border-radius: 15px; + border: 1px solidrgb(151, 149, 149); + padding: 8px; + } + )"); + + ui->balancesCard->setStyleSheet(R"( QFrame#balancesCard { background: #FFFFFF; border-radius: 15px; border: 1px solidrgb(151, 149, 149); padding: 8px; } )"); + + auto *filterGroup = new QButtonGroup(this); + filterGroup->addButton(ui->btnFilterAll); + filterGroup->addButton(ui->btnFilterFIRO); + filterGroup->addButton(ui->btnFilterAssets); + filterGroup->setExclusive(true); + ui->btnFilterAll->setChecked(true); + + connect(filterGroup, QOverload::of(&QButtonGroup::buttonClicked), this, [this](QAbstractButton *button){ + if (!filter) { return; } + if (button == ui->btnFilterAll) { + filter->setTypeFilter(TransactionFilterProxy::ALL_TYPES); + } else if (button == ui->btnFilterFIRO) { + quint32 mask = MakeTypeMask({ TransactionRecord::Other, + TransactionRecord::Generated, + TransactionRecord::SendToAddress, + TransactionRecord::SendToOther, + TransactionRecord::RecvWithAddress, + TransactionRecord::RecvFromOther, + TransactionRecord::SendToSelf, + TransactionRecord::SpendToAddress, + TransactionRecord::SpendToSelf, + TransactionRecord::Anonymize, + TransactionRecord::SendToPcode, + TransactionRecord::RecvWithPcode, + TransactionRecord::MintSparkToSelf, + TransactionRecord::SpendSparkToSelf, + TransactionRecord::MintSparkTo, + TransactionRecord::SpendSparkTo, + TransactionRecord::RecvSpark }); + filter->setTypeFilter(mask); + } else if (button == ui->btnFilterAssets) { + quint32 mask = MakeTypeMask({ TransactionRecord::SpatsCreate, + TransactionRecord::SpatsMint, + TransactionRecord::SpatsModify, + TransactionRecord::SpatsRevoke }); + filter->setTypeFilter(mask); + } + }); + + connect(ui->searchSparkAsset, &QLineEdit::textChanged, this, [this](const QString &text) { + const auto frames = ui->scrollWidgetSparkAssets->findChildren("assetRow"); + for (auto *frame : frames) { + bool match = false; + for (auto *label : frame->findChildren()) { + if (label->text().contains(text, Qt::CaseInsensitive)) { + match = true; + break; + } + } + frame->setVisible(match || text.isEmpty()); + } + }); - if(torEnabled){ - ui->checkboxEnabledTor->setChecked(true); - }else{ - ui->checkboxEnabledTor->setChecked(false); + connect(ui->sendButton, &QPushButton::clicked, this, [this]() { Q_EMIT gotoSendCoinsPage(); }); + connect(ui->receiveButton, &QPushButton::clicked, this, [this]() { Q_EMIT gotoReceiveCoinsPage(); }); + + addShadow(ui->balancesCard); + addShadow(ui->sparkCard); + addShadow(ui->activityCard); + addShadow(ui->warningFrame); + + { + QFont totalFont; + totalFont.setFamily("Segoe UI"); + totalFont.setPointSize(30); + totalFont.setBold(true); + QFont totalVal; + totalVal.setFamily("Segoe UI"); + totalVal.setPointSize(30); + totalVal.setBold(false); + + ui->labelTotalText->setFont(totalFont); + ui->labelTotal->setFont(totalVal); + ui->labelTotalText->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); + ui->labelTotal->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); } QIcon icon = QIcon(":/icons/warning"); - icon.addPixmap(icon.pixmap(QSize(64,64), QIcon::Normal), QIcon::Disabled); // also set the disabled icon because we are using a disabled QPushButton to work around missing HiDPI support of QLabel (https://bugreports.qt.io/browse/QTBUG-42503) - ui->labelTransactionsStatus->setIcon(icon); - ui->labelWalletStatus->setIcon(icon); + icon.addPixmap(icon.pixmap(QSize(64,64), QIcon::Normal), QIcon::Disabled); - // Recent transactions ui->listTransactions->setItemDelegate(txdelegate); ui->listTransactions->setIconSize(QSize(DECORATION_SIZE, DECORATION_SIZE)); - ui->listTransactions->setMinimumHeight(NUM_ITEMS * (DECORATION_SIZE + 2)); + ui->listTransactions->setMinimumHeight(NUM_ITEMS * (DECORATION_SIZE + 16)); ui->listTransactions->setAttribute(Qt::WA_MacShowFocusRect, false); connect(ui->listTransactions, &QListView::clicked, this, &OverviewPage::handleTransactionClicked); connect(ui->checkboxEnabledTor, &QCheckBox::toggled, this, &OverviewPage::handleEnabledTorChanged); - - // start with displaying the "out of sync" warnings - showOutOfSyncWarning(true); - connect(ui->labelWalletStatus, &QPushButton::clicked, this, &OverviewPage::handleOutOfSyncWarningClicks); - connect(ui->labelTransactionsStatus, &QPushButton::clicked, this, &OverviewPage::handleOutOfSyncWarningClicks); - connect(&countDownTimer, &QTimer::timeout, this, &OverviewPage::countDown); countDownTimer.start(30000); + connect(ui->migrateButton, &QPushButton::clicked, this, &OverviewPage::migrateClicked); + connect(this, &OverviewPage::spatsRegistryChangedSignal, this, &OverviewPage::handleSpatsRegistryChangedSignal); - ui->tableWidgetSparkBalances->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->tableWidgetSparkBalances, &QTableWidget::customContextMenuRequested, this, &OverviewPage::on_tableWidgetSparkBalances_contextMenuRequested); + showOutOfSyncWarning(true); - connect(this, &OverviewPage::spatsRegistryChangedSignal, this, &OverviewPage::handleSpatsRegistryChangedSignal); + bool torEnabled; + if(IsArgSet("-torsetup")){ + torEnabled = GetBoolArg("-torsetup", DEFAULT_TOR_SETUP); + }else{ + torEnabled = settings.value("fTorSetup").toBool(); + } + ui->checkboxEnabledTor->setChecked(torEnabled); + + ui->labelTotalText->setStyleSheet(R"( QLabel { font-size: 22pt; font-weight: 700; color: #111827; } )"); + ui->labelTotal->setStyleSheet(R"( QLabel { font-size: 22pt; font-weight: 700; color: #111827; } )"); + + ui->pageSparkAssets->setStyleSheet(R"( QWidget#pageSparkAssets { background: #FFFFFF; border: none; outline: none; margin: 0; padding: 0; } )"); + + ui->miniBalancesRow->setSpacing(2); + + ui->anonymizeButton->setMinimumHeight(36); + ui->sendButton->setMinimumHeight(36); + ui->receiveButton->setMinimumHeight(36); +} + +void OverviewPage::addShadow(QWidget *w) +{ + auto *shadow = new QGraphicsDropShadowEffect(this); + shadow->setBlurRadius(18); + shadow->setOffset(0, 4); + shadow->setColor(QColor(0, 0, 0, 60)); + w->setGraphicsEffect(shadow); } void OverviewPage::handleTransactionClicked(const QModelIndex &index) @@ -208,10 +343,9 @@ void OverviewPage::handleTransactionClicked(const QModelIndex &index) Q_EMIT transactionClicked(filter->mapToSource(index)); } -void OverviewPage::handleEnabledTorChanged(){ - +void OverviewPage::handleEnabledTorChanged() +{ QMessageBox msgBox; - if(ui->checkboxEnabledTor->isChecked()){ settings.setValue("fTorSetup", true); msgBox.setText(tr("Please restart the Firo wallet to route your connection through Tor to protect your IP address.
Syncing your wallet might be slower with Tor.
Note that -torsetup in firo.conf will always override any changes made here.")); @@ -227,11 +361,6 @@ void OverviewPage::handleOutOfSyncWarningClicks() Q_EMIT outOfSyncWarningClicked(); } -auto &getSpatsManager() -{ - return spark::CSparkState::GetState()->GetSpatsManager(); -} - OverviewPage::~OverviewPage() { getSpatsManager().remove_updates_observer( *this ); @@ -243,22 +372,27 @@ void OverviewPage::on_anonymizeButton_clicked() if (!walletModel) { return; } - if (spark::IsSparkAllowed()) { auto sparkModel = walletModel->getSparkModel(); if (!sparkModel) { return; } - sparkModel->mintSparkAll(AutoMintSparkMode::MintAll); } } void OverviewPage::setBalance( - const CAmount& balance, const CAmount& unconfirmedBalance, const CAmount& immatureBalance, - const CAmount& watchOnlyBalance, const CAmount& watchUnconfBalance, const CAmount& watchImmatureBalance, - const spats::Wallet::asset_balances_t& spats_balances, const CAmount& anonymizableBalance) + const CAmount& balance, + const CAmount& unconfirmedBalance, + const CAmount& immatureBalance, + const CAmount& watchOnlyBalance, + const CAmount& watchUnconfBalance, + const CAmount& watchImmatureBalance, + const spats::Wallet::asset_balances_t& spats_balances, + const CAmount& anonymizableBalance) { + if (!walletModel || !walletModel->getOptionsModel()) return; + int unit = walletModel->getOptionsModel()->getDisplayUnit(); currentBalance = balance; currentUnconfirmedBalance = unconfirmedBalance; @@ -266,62 +400,85 @@ void OverviewPage::setBalance( currentWatchOnlyBalance = watchOnlyBalance; currentWatchUnconfBalance = watchUnconfBalance; currentWatchImmatureBalance = watchImmatureBalance; - if (currentSpatsBalances_ != spats_balances) // optimization, probably + + if (currentSpatsBalances_ != spats_balances) currentSpatsBalances_ = std::move(spats_balances); + currentAnonymizableBalance = anonymizableBalance; + const auto [privateBalance, unconfirmedPrivateBalance] = currentSpatsBalances_[spats::base::universal_id]; - ui->labelBalance->setText(BitcoinUnits::formatWithUnit(unit, balance, false, BitcoinUnits::separatorAlways)); - ui->labelUnconfirmed->setText(BitcoinUnits::formatWithUnit(unit, unconfirmedBalance, false, BitcoinUnits::separatorAlways)); - ui->labelImmature->setText(BitcoinUnits::formatWithUnit(unit, immatureBalance, false, BitcoinUnits::separatorAlways)); - ui->labelTotal->setText(BitcoinUnits::formatWithUnit(unit, balance + unconfirmedBalance + immatureBalance + privateBalance.raw() + unconfirmedPrivateBalance.raw(), false, BitcoinUnits::separatorAlways)); - ui->labelWatchAvailable->setText(BitcoinUnits::formatWithUnit(unit, watchOnlyBalance, false, BitcoinUnits::separatorAlways)); - ui->labelWatchPending->setText(BitcoinUnits::formatWithUnit(unit, watchUnconfBalance, false, BitcoinUnits::separatorAlways)); - ui->labelWatchImmature->setText(BitcoinUnits::formatWithUnit(unit, watchImmatureBalance, false, BitcoinUnits::separatorAlways)); - ui->labelWatchTotal->setText(BitcoinUnits::formatWithUnit(unit, watchOnlyBalance + watchUnconfBalance + watchImmatureBalance, false, BitcoinUnits::separatorAlways)); - ui->labelAnonymizable->setText(BitcoinUnits::formatWithUnit(unit, anonymizableBalance, false, BitcoinUnits::separatorAlways)); - displaySpatsBalances(); + + ui->labelTransparentAvailableText->setText(BitcoinUnits::formatWithUnit(unit, balance, false, BitcoinUnits::separatorAlways)); + ui->labelTransparentPendingText->setText(BitcoinUnits::formatWithUnit(unit, unconfirmedBalance, false, BitcoinUnits::separatorAlways)); + ui->labelTransparentImmatureText->setText(BitcoinUnits::formatWithUnit(unit, immatureBalance, false, BitcoinUnits::separatorAlways)); + ui->labelPrivateAvailableText->setText(BitcoinUnits::formatWithUnit(unit, privateBalance.raw(), false, BitcoinUnits::separatorAlways)); + + ui->labelTotal->setText( + BitcoinUnits::formatWithUnit( + unit, + balance + unconfirmedBalance + immatureBalance + privateBalance.raw() + unconfirmedPrivateBalance.raw(), + false, + BitcoinUnits::separatorAlways + ) + ); ui->anonymizeButton->setEnabled(spark::IsSparkAllowed() && anonymizableBalance > 0); - // only show immature (newly mined) balance if it's non-zero, so as not to complicate things - // for the non-mining users + displaySpatsBalances(); + bool showImmature = immatureBalance != 0; bool showWatchOnlyImmature = watchImmatureBalance != 0; - - // for symmetry reasons also show immature label when the watch-only one is shown - ui->labelImmature->setVisible(showImmature || showWatchOnlyImmature); - ui->labelImmatureText->setVisible(showImmature || showWatchOnlyImmature); - ui->labelWatchImmature->setVisible(showWatchOnlyImmature); // show watch-only immature balance + ui->labelTransparentImmatureText->setVisible(showImmature || showWatchOnlyImmature); } void OverviewPage::displaySpatsBalances() { - auto& table_widget = *ui->tableWidgetSparkBalances; - table_widget.clearContents(); - table_widget.setRowCount( currentSpatsBalances_.size() ); - int row = 0; - for ( const auto& [asset_id, balance] : currentSpatsBalances_ ) { - // Fill the table with all attributes to be displayed, to the extent possible - table_widget.setItem( row, ColumnAssetType, new QTableWidgetItem( QString::number( utils::to_underlying( asset_id.first ) ) ) ); - table_widget.setItem( row, ColumnIdentifier, new QTableWidgetItem( QString::number( utils::to_underlying( asset_id.second ) ) ) ); - table_widget.setItem( row, ColumnAvailableBalance, new QTableWidgetItem( QString::fromStdString( boost::lexical_cast< std::string >( balance.available ) ) ) ); - table_widget.setItem( row, ColumnPendingBalance, new QTableWidgetItem( QString::fromStdString( boost::lexical_cast< std::string >( balance.pending ) ) ) ); - if ( const spats::SparkAssetDisplayAttributes* const a = getSpatsDisplayAttributes( asset_id ) ) { - table_widget.setItem( row, ColumnName, new QTableWidgetItem( QString::fromStdString( a->name ) ) ); - table_widget.setItem( row, ColumnSymbol, new QTableWidgetItem( QString::fromStdString( a->symbol ) ) ); - table_widget.setItem( row, ColumnFungible, new QTableWidgetItem( a->fungible ? "Yes" : "No" ) ); - table_widget.setItem( row, ColumnMetadata, new QTableWidgetItem( QString::fromStdString( a->metadata ) ) ); - table_widget.setItem( row, ColumnDescription, new QTableWidgetItem( QString::fromStdString( a->description ) ) ); - } - else - table_widget.setItem( row, ColumnFungible, new QTableWidgetItem( is_fungible_asset_type( asset_id.first ) ? "Yes" : "No" ) ); - - // Make the table items read-only to prevent user editing - for ( int col = ColumnAssetType; col < ColumnCount; ++col ) - if ( auto * const item = table_widget.item( row, col ) ) - item->setFlags( item->flags() & ~Qt::ItemIsEditable ); - ++row; + QLayoutItem *child; + while ((child = ui->layoutSparkAssetsList->takeAt(0)) != nullptr) { + if (auto *w = child->widget()) { + w->deleteLater(); + } + delete child; + } + + for (const auto& [asset_id, balance] : currentSpatsBalances_) { + QString idText = QString::number(static_cast(asset_id.first)) + ":" + QString::number(static_cast(asset_id.second)); + QString nameText = "Unknown"; + if (const auto* a = getSpatsDisplayAttributes(asset_id)) + nameText = QString::fromStdString(a->name); + + QString availableText = QString::fromStdString(boost::lexical_cast(balance.available)); + + auto *frame = new QFrame(ui->scrollWidgetSparkAssets); + frame->setObjectName("assetRow"); + frame->setStyleSheet(R"( QFrame#assetRow { background: #FFFFFF; border: none; padding: 6px 10px; } )"); + + auto *rowLayout = new QHBoxLayout(frame); + rowLayout->setSpacing(16); + rowLayout->setContentsMargins(12, 2, 12, 2); + + QString labelStyle = R"( QLabel { color: #6B7280; font-size: 13pt; font-weight: 500; background: transparent; } )"; + + auto *idLabel = new QLabel(idText, frame); + idLabel->setStyleSheet(QString(R"( QLabel { color: #6B7280; font-size: 13pt; font-weight: 600; background: transparent; padding-left: 20px; /* лёгкий сдвиг вправо под заголовок Asset ID */ } )")); + + auto *nameLabel = new QLabel(nameText, frame); + nameLabel->setStyleSheet(QString(R"( QLabel { color: #6B7280; font-size: 13pt; font-weight: 500; background: transparent; padding-left: 40px; /* добавлено: смещение текста под заголовок Name */ } )")); + + auto *spacer = new QSpacerItem(20, 10, QSizePolicy::Expanding, QSizePolicy::Minimum); + + auto *balanceLabel = new QLabel(availableText, frame); + balanceLabel->setStyleSheet(R"( QLabel { color: #6B7280; font-size: 13pt; font-weight: 600; background: transparent; } )"); + + rowLayout->addWidget(idLabel); + rowLayout->addSpacing(60); + rowLayout->addWidget(nameLabel); + rowLayout->addItem(spacer); + rowLayout->addWidget(balanceLabel); + + ui->layoutSparkAssetsList->addWidget(frame); } + ui->layoutSparkAssetsList->addStretch(); } const spats::SparkAssetDisplayAttributes* OverviewPage::getSpatsDisplayAttributes( spats::universal_asset_id_t asset_id ) @@ -332,11 +489,9 @@ const spats::SparkAssetDisplayAttributes* OverviewPage::getSpatsDisplayAttribute const auto old_size = spats_display_attributes_cache_.size(); it = spats_display_attributes_cache_.emplace( asset_id, spats::SparkAssetDisplayAttributes( located_asset->asset ) ).first; const auto new_size = spats_display_attributes_cache_.size(); - if ( old_size == 0 && new_size > 0 ) // first time we put something in the cache - getSpatsManager().add_updates_observer( *this ); // so now set up listener for registry changes, so that we can change the cache as necessary - // Given how handleSpatsRegistryChangedSignal() is implemented, the above call might actually happen multiple times. It's a no-op after the first, so no big deal. - } - else { + if ( old_size == 0 && new_size > 0 ) + getSpatsManager().add_updates_observer( *this ); + } else { LogPrintf( "Failed to find asset {%u, %u} in spats registry!\n", asset_id.first, asset_id.second ); return nullptr; } @@ -346,12 +501,8 @@ const spats::SparkAssetDisplayAttributes* OverviewPage::getSpatsDisplayAttribute void OverviewPage::process_spats_registry_changed( const admin_addresses_set_t &/*affected_asset_admin_addresses*/, const asset_ids_set_t &affected_asset_ids ) { - // Ideally I'd like to do this: Q_EMIT spatsRegistryChangedSignal( affected_asset_ids ); - // But apparently it's a problem to pass std::unordered_set through Qt's signal-slot mechanism. - // Thus a more complicated alternative below... { std::lock_guard lock( spats_registry_change_affected_asset_ids_mutex_ ); - // adding on top of what already may be there... spats_registry_change_affected_asset_ids_.insert( affected_asset_ids.begin(), affected_asset_ids.end() ); } Q_EMIT spatsRegistryChangedSignal(); @@ -361,81 +512,24 @@ void OverviewPage::handleSpatsRegistryChangedSignal() { { std::lock_guard lock( spats_registry_change_affected_asset_ids_mutex_ ); - // removing entries from the cache where they might have been affected by the changes in the registry - erase_if( spats_display_attributes_cache_, - [this]( const auto& entry ) { - const auto [asset_type, identifier] = entry.first; - return spats_registry_change_affected_asset_ids_.contains( { asset_type, std::nullopt } ) || - spats_registry_change_affected_asset_ids_.contains( { asset_type, identifier } ); + erase_if( spats_display_attributes_cache_, [this]( const auto& entry ) { + const auto [asset_type, identifier] = entry.first; + return spats_registry_change_affected_asset_ids_.contains( { asset_type, std::nullopt } ) || + spats_registry_change_affected_asset_ids_.contains( { asset_type, identifier } ); } ); spats_registry_change_affected_asset_ids_.clear(); } - // now update the display, which will fetch the attributes from the registry again for those keys that have just been removed from the cache displaySpatsBalances(); } -void OverviewPage::on_tableWidgetSparkBalances_contextMenuRequested( const QPoint &pos ) -{ - // Get the selected row - QModelIndex index = ui->tableWidgetSparkBalances->indexAt( pos ); - if ( !index.isValid() ) { - return; - } - - const int row = index.row(); - const spats::asset_type_t asset_type{ ui->tableWidgetSparkBalances->item( row, ColumnAssetType )->text().toULongLong() }; - if ( !is_fungible_asset_type( asset_type ) ) - return; // No burning for NFTs - - if ( asset_type == spats::base::asset_type ) - return; // No burning for the base asset (This change was decided at the 2025-08-05 meeting) - - using balance_type = spats::Wallet::amount_type; - balance_type balance; - try { - balance = balance_type::from_string( ui->tableWidgetSparkBalances->item( row, ColumnAvailableBalance )->text().toStdString() ); - } - catch ( ... ) { - return; - } - if ( balance <= balance_type{} ) - return; // No burning unless the balance is positive - - const spats::asset_symbol_t asset_symbol{ ui->tableWidgetSparkBalances->item( row, ColumnSymbol )->text().toStdString() }; - - // Create context menu - QMenu context_menu( this ); - QAction *burn_action = context_menu.addAction( tr("Burn") ); - - // Connect the action to open the burn dialog - connect( burn_action, &QAction::triggered, this, [this, row, asset_type, asset_symbol, balance] { - try { - // Show the burn dialog - SpatsBurnDialog dialog( txdelegate->platformStyle, asset_type, ui->tableWidgetSparkBalances->item( row, ColumnSymbol )->text().toStdString(), - spats::supply_amount_t( balance.raw() , balance.precision() ), this ); - if ( dialog.exec() == QDialog::Accepted ) - walletModel->getWallet()->BurnSparkAssetSupply( asset_type, asset_symbol, dialog.getBurnAmount(), - MakeSpatsUserConfirmationCallback( *walletModel, this ) ); - } - catch ( const std::exception &e ) { - QMessageBox::critical( this, tr( "Error" ), tr( "An error occurred: %1" ).arg( e.what() ) ); - } - } ); - - // Show the context menu - context_menu.exec( ui->tableWidgetSparkBalances->viewport()->mapToGlobal( pos ) ); -} - -// show/hide watch-only labels void OverviewPage::updateWatchOnlyLabels(bool showWatchOnly) { - ui->labelSpendable->setVisible(showWatchOnly); // show spendable label (only when watch-only is active) - ui->labelWatchonly->setVisible(showWatchOnly); // show watch-only label - ui->lineWatchBalance->setVisible(showWatchOnly); // show watch-only balance separator line - ui->labelWatchAvailable->setVisible(showWatchOnly); // show watch-only available balance - ui->labelWatchPending->setVisible(showWatchOnly); // show watch-only pending balance - ui->labelWatchTotal->setVisible(showWatchOnly); // show watch-only total balance - + ui->labelSpendable->setVisible(showWatchOnly); + ui->labelWatchonly->setVisible(showWatchOnly); + ui->lineWatchBalance->setVisible(showWatchOnly); + ui->labelWatchAvailable->setVisible(showWatchOnly); + ui->labelWatchPending->setVisible(showWatchOnly); + ui->labelWatchTotal->setVisible(showWatchOnly); if (!showWatchOnly) ui->labelWatchImmature->hide(); } @@ -443,10 +537,8 @@ void OverviewPage::updateWatchOnlyLabels(bool showWatchOnly) void OverviewPage::setClientModel(ClientModel *model) { this->clientModel = model; - if(model) - { + if(model) { connect(model, &ClientModel::numBlocksChanged, this, &OverviewPage::onRefreshClicked); - // Show warning if this is a prerelease version connect(model, &ClientModel::alertsChanged, this, &OverviewPage::updateAlerts); updateAlerts(model->getStatusBarWarnings()); } @@ -456,9 +548,8 @@ void OverviewPage::setWalletModel(WalletModel *model) { this->walletModel = model; onRefreshClicked(); - if(model && model->getOptionsModel()) - { - // Set up transaction list + + if(model && model->getOptionsModel()) { filter.reset(new TransactionFilterProxy()); filter->setSourceModel(model->getTransactionTableModel()); filter->setLimit(NUM_ITEMS); @@ -466,44 +557,38 @@ void OverviewPage::setWalletModel(WalletModel *model) filter->setSortRole(Qt::EditRole); filter->setShowInactive(false); filter->sort(TransactionTableModel::Date, Qt::DescendingOrder); + filter->setTypeFilter(TransactionFilterProxy::ALL_TYPES); ui->listTransactions->setModel(filter.get()); ui->listTransactions->setModelColumn(TransactionTableModel::ToAddress); - // Keep up to date with wallet setBalance( - model->getBalance(), - model->getUnconfirmedBalance(), - model->getImmatureBalance(), - model->getWatchBalance(), - model->getWatchUnconfirmedBalance(), - model->getWatchImmatureBalance(), - walletModel->getSpatsBalances(), - model->getAnonymizableBalance()); - connect(model, &WalletModel::balanceChanged, this, &OverviewPage::setBalance); + model->getBalance(), + model->getUnconfirmedBalance(), + model->getImmatureBalance(), + model->getWatchBalance(), + model->getWatchUnconfirmedBalance(), + model->getWatchImmatureBalance(), + walletModel->getSpatsBalances(), + model->getAnonymizableBalance() + ); + connect(model, &WalletModel::balanceChanged, this, &OverviewPage::setBalance); connect(model->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &OverviewPage::updateDisplayUnit); - updateWatchOnlyLabels(model->haveWatchOnly()); connect(model, &WalletModel::notifyWatchonlyChanged, this, &OverviewPage::updateWatchOnlyLabels); } - - // update the display unit, to not use the default ("BTC") updateDisplayUnit(); } void OverviewPage::updateDisplayUnit() { - if(walletModel && walletModel->getOptionsModel()) - { + if(walletModel && walletModel->getOptionsModel()) { if(currentBalance != -1) setBalance(currentBalance, currentUnconfirmedBalance, currentImmatureBalance, currentWatchOnlyBalance, currentWatchUnconfBalance, currentWatchImmatureBalance, currentSpatsBalances_, currentAnonymizableBalance); - - // Update txdelegate->unit with the current unit txdelegate->unit = walletModel->getOptionsModel()->getDisplayUnit(); - ui->listTransactions->update(); } } @@ -516,8 +601,8 @@ void OverviewPage::updateAlerts(const QString &warnings) void OverviewPage::showOutOfSyncWarning(bool fShow) { - ui->labelWalletStatus->setVisible(fShow); - ui->labelTransactionsStatus->setVisible(fShow); + // ui->labelWalletStatus->setVisible(fShow); + // ui->labelTransactionsStatus->setVisible(fShow); } void OverviewPage::countDown() @@ -533,6 +618,8 @@ void OverviewPage::countDown() void OverviewPage::onRefreshClicked() { + if (!walletModel) return; + size_t confirmed, unconfirmed; auto privateBalance = walletModel->getWallet()->GetPrivateBalance(confirmed, unconfirmed); auto lGracefulPeriod = ::Params().GetConsensus().nLelantusGracefulPeriod; @@ -558,6 +645,8 @@ void OverviewPage::onRefreshClicked() void OverviewPage::migrateClicked() { + if (!walletModel) return; + size_t confirmed, unconfirmed; auto privateBalance = walletModel->getWallet()->GetPrivateBalance(confirmed, unconfirmed); auto lGracefulPeriod = ::Params().GetConsensus().nLelantusGracefulPeriod; @@ -566,7 +655,6 @@ void OverviewPage::migrateClicked() QString info = tr("Your wallet needs to be unlocked to migrate your funds to Spark."); if(walletModel->getEncryptionStatus() == WalletModel::Locked) { - AskPassphraseDialog dlg(AskPassphraseDialog::Unlock, this, info); dlg.setModel(walletModel); dlg.exec(); @@ -580,60 +668,64 @@ void OverviewPage::migrateClicked() } } } + MigrateLelantusToSparkDialog::MigrateLelantusToSparkDialog(WalletModel *_model):QMessageBox() { - this->model = _model; - QDialog::setWindowTitle("Migrate funds from Lelantus to Spark"); - QDialog::setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint); - - QLabel *ic = new QLabel(); - QIcon icon_; - icon_.addFile(QString::fromUtf8(":/icons/ic_info"), QSize(), QIcon::Normal, QIcon::On); - ic->setPixmap(icon_.pixmap(18, 18)); - ic->setFixedWidth(90); - ic->setAlignment(Qt::AlignRight); - ic->setStyleSheet("color:#92400E"); - - QLabel *text = new QLabel(); - text->setText(tr("Firo is migrating to Spark. Please migrate your funds.")); - text->setAlignment(Qt::AlignLeft); - text->setWordWrap(true); - text->setStyleSheet("color:#92400E;text-align:center;word-wrap: break-word;"); - - QPushButton *ignore = new QPushButton(this); - ignore->setText("Ignore"); - ignore->setStyleSheet("margin-top:30px;margin-bottom:60px;margin-left:20px;margin-right:50px;"); - QPushButton *migrate = new QPushButton(this); - migrate->setText("Migrate"); - migrate->setStyleSheet("color:#9b1c2e;background-color:none;margin-top:30px;margin-bottom:60px;margin-left:50px;margin-right:20px;border:1px solid #9b1c2e;"); - QHBoxLayout *groupButton = new QHBoxLayout(this); - groupButton->addWidget(ignore); - groupButton->addWidget(migrate); - - QHBoxLayout *hlayout = new QHBoxLayout(this); - hlayout->addWidget(ic); - hlayout->addWidget(text); - - QWidget *layout_ = new QWidget(); - layout_->setLayout(hlayout); - layout_->setStyleSheet("background-color:#FEF3C7;"); - - QVBoxLayout *vlayout = new QVBoxLayout(this); - vlayout->addWidget(layout_); - vlayout->addLayout(groupButton); - vlayout->setContentsMargins(0,0,0,0); - - QWidget *wbody = new QWidget(); - wbody->setLayout(vlayout); - - layout()->addWidget(wbody); - setContentsMargins(0, 0, 0, 0); - setStyleSheet("margin-right:-30px;"); - setStandardButtons(StandardButtons()); - - connect(ignore, &QPushButton::clicked, this, &MigrateLelantusToSparkDialog::onIgnoreClicked); - connect(migrate, &QPushButton::clicked, this, &MigrateLelantusToSparkDialog::onMigrateClicked); - exec(); + this->model = _model; + QDialog::setWindowTitle("Migrate funds from Lelantus to Spark"); + QDialog::setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint); + + QLabel *ic = new QLabel(); + QIcon icon_; + icon_.addFile(QString::fromUtf8(":/icons/ic_info"), QSize(), QIcon::Normal, QIcon::On); + ic->setPixmap(icon_.pixmap(18, 18)); + ic->setFixedWidth(90); + ic->setAlignment(Qt::AlignRight); + ic->setStyleSheet("color:#92400E"); + + QLabel *text = new QLabel(); + text->setText(tr("Firo is migrating to Spark. Please migrate your funds.")); + text->setAlignment(Qt::AlignLeft); + text->setWordWrap(true); + text->setStyleSheet("color:#92400E;text-align:center;word-wrap: break-word;"); + + QPushButton *ignore = new QPushButton(this); + ignore->setText("Ignore"); + ignore->setStyleSheet("margin-top:30px;margin-bottom:60px;margin-left:20px;margin-right:50px;"); + + QPushButton *migrate = new QPushButton(this); + migrate->setText("Migrate"); + migrate->setStyleSheet("color:#9b1c2e;background-color:none;margin-top:30px;margin-bottom:60px;margin-left:50px;margin-right:20px;border:1px solid #9b1c2e;"); + + QHBoxLayout *groupButton = new QHBoxLayout(this); + groupButton->addWidget(ignore); + groupButton->addWidget(migrate); + + QHBoxLayout *hlayout = new QHBoxLayout(this); + hlayout->addWidget(ic); + hlayout->addWidget(text); + + QWidget *layout_ = new QWidget(); + layout_->setLayout(hlayout); + layout_->setStyleSheet("background-color:#FEF3C7;"); + + QVBoxLayout *vlayout = new QVBoxLayout(this); + vlayout->addWidget(layout_); + vlayout->addLayout(groupButton); + vlayout->setContentsMargins(0,0,0,0); + + QWidget *wbody = new QWidget(); + wbody->setLayout(vlayout); + layout()->addWidget(wbody); + + setContentsMargins(0, 0, 0, 0); + setStyleSheet("margin-right:-30px;"); + setStandardButtons(StandardButtons()); + + connect(ignore, &QPushButton::clicked, this, &MigrateLelantusToSparkDialog::onIgnoreClicked); + connect(migrate, &QPushButton::clicked, this, &MigrateLelantusToSparkDialog::onMigrateClicked); + + exec(); } void MigrateLelantusToSparkDialog::onIgnoreClicked() @@ -656,98 +748,49 @@ bool MigrateLelantusToSparkDialog::getClickedButton() void OverviewPage::resizeEvent(QResizeEvent* event) { - QWidget::resizeEvent(event); - - // Retrieve new dimensions from the resize event + QWidget::resizeEvent(event); const int newWidth = event->size().width(); const int newHeight = event->size().height(); - adjustTextSize(newWidth, newHeight); + const int maxWidth = 1920; + const int maxHeight = 1200; - // Determine widths for specific widgets as percentages of total width - int labelWidth = static_cast(newWidth * 0.5); - int labelMinWidth = static_cast(newWidth * 0.15); - int labelMaxWidth = static_cast(newWidth * 0.35); - const int labelHeight = 20; - - // Configure the dimensions and constraints of each widget - ui->labelBalance->setFixedWidth(labelWidth); - ui->labelBalance->setMinimumWidth(labelMinWidth); - ui->labelBalance->setMaximumWidth(labelMaxWidth); - ui->labelBalance->setFixedHeight(labelHeight); - - ui->labelUnconfirmed->setFixedWidth(labelWidth); - ui->labelUnconfirmed->setMinimumWidth(labelMinWidth); - ui->labelUnconfirmed->setMaximumWidth(labelMaxWidth); - ui->labelUnconfirmed->setFixedHeight(labelHeight); - - int buttonWidth = static_cast(newWidth * 0.15); - int buttonHeight = static_cast(newHeight * 0.05); - int buttonMinHeight = static_cast(20); - int buttonMaxHeight = static_cast(45); - - ui->anonymizeButton->setMinimumWidth(buttonWidth); - ui->anonymizeButton->setMaximumWidth(buttonWidth * 2); - ui->anonymizeButton->setMinimumHeight(buttonMinHeight); - ui->anonymizeButton->setMaximumHeight(buttonMaxHeight); - - // Set the minimum width for all label widgets to ensure they maintain a consistent and readable size regardless of window resizing - ui->labelAnonymizable->setMinimumWidth(labelMinWidth); - ui->labelAlerts->setMinimumWidth(labelMinWidth); - ui->label->setMinimumWidth(labelMinWidth); - ui->labelWatchPending->setMinimumWidth(labelMinWidth); - ui->labelBalance->setMinimumWidth(labelMinWidth); - ui->labelSpendable->setMinimumWidth(labelMinWidth); - ui->labelWatchAvailable->setMinimumWidth(labelMinWidth); - ui->labelWatchonly->setMinimumWidth(labelMinWidth); - ui->labelTotal->setMinimumWidth(labelMinWidth); - ui->labelWatchTotal->setMinimumWidth(labelMinWidth); - ui->labelUnconfirmed->setMinimumWidth(labelMinWidth); - ui->labelImmature->setMinimumWidth(labelMinWidth); - ui->label_4->setMinimumWidth(labelMinWidth); -} + if (newWidth > maxWidth || newHeight > maxHeight) { + resize(std::min(newWidth, maxWidth), std::min(newHeight, maxHeight)); + return; + } -void OverviewPage::adjustTextSize(int width, int height){ - - const double fontSizeScalingFactor = 133.0; - int baseFontSize = width / fontSizeScalingFactor; - int fontSize = std::min(15, std::max(12, baseFontSize)); - - // Font for regular text components(not bold) - QFont textFont = ui->labelBalance->font(); - textFont.setPointSize(fontSize); - textFont.setBold(false); - - // Font for text components that should be bold - QFont labelFont = textFont; - labelFont.setBold(true); - - ui->textWarning1->setFont(textFont); - ui->textWarning2->setFont(textFont); - ui->labelWalletStatus->setFont(textFont); - ui->anonymizeButton->setFont(textFont); - - // Apply label font to all label components - ui->labelAlerts->setFont(labelFont); - ui->label_5->setFont(labelFont); - ui->labelAnonymizableText->setFont(textFont); - ui->label->setFont(labelFont); - ui->labelAnonymizable->setFont(labelFont); - ui->labelWatchPending->setFont(labelFont); - ui->labelBalance->setFont(labelFont); - ui->labelSpendable->setFont(labelFont); - ui->labelWatchAvailable->setFont(labelFont); - ui->labelPendingText->setFont(textFont); - ui->tableWidgetSparkBalances->setFont(textFont); - ui->tableWidgetSparkBalances->horizontalHeader()->setFont(labelFont); - ui->tableWidgetSparkBalances->verticalHeader()->setFont(labelFont); - ui->labelTotalText->setFont(textFont); - ui->labelWatchonly->setFont(labelFont); - ui->labelBalanceText->setFont(textFont); - ui->labelTotal->setFont(labelFont); - ui->labelWatchTotal->setFont(labelFont); - ui->labelUnconfirmed->setFont(labelFont); - ui->labelImmatureText->setFont(textFont); - ui->labelImmature->setFont(labelFont); - ui->labelWatchImmature->setFont(labelFont); - ui->label_4->setFont(labelFont); + adjustTextSize(newWidth, newHeight); } + +void OverviewPage::adjustTextSize(int width, int /*height*/) +{ + const double fontScale = 133.0; + int base = std::max(12, std::min(15, width / (int)fontScale)); + + QFont baseFont("Segoe UI", base, QFont::Normal); + QFont boldFont("Segoe UI", base, QFont::DemiBold); + auto hintFont = baseFont; + hintFont.setPointSize(std::max(11, base - 1)); + + ui->textWarning1->setFont(baseFont); + ui->textWarning2->setFont(baseFont); + ui->anonymizeButton->setFont(boldFont); + ui->labelAlerts->setFont(boldFont); + + ui->labelPrivateAvailableTitle->setFont(hintFont); + ui->labelPrivateAvailableText->setFont(boldFont); + ui->labelPrivatePendingTitle->setFont(hintFont); + ui->labelPrivatePendingText->setFont(boldFont); + ui->labelTransparentAvailableTitle->setFont(hintFont); + ui->labelTransparentAvailableText->setFont(boldFont); + ui->labelTransparentPendingTitle->setFont(hintFont); + ui->labelTransparentPendingText->setFont(boldFont); + ui->labelTransparentImmatureTitle->setFont(hintFont); + ui->labelTransparentImmatureText->setFont(boldFont); + + QFont totalFont("Segoe UI", 30, QFont::Bold); + QFont totalVal("Segoe UI", 30, QFont::Normal); + + ui->labelTotalText->setFont(totalFont); + ui->labelTotal->setFont(totalVal); +} \ No newline at end of file diff --git a/src/qt/overviewpage.h b/src/qt/overviewpage.h index 6cea04daf8..b0eed537df 100644 --- a/src/qt/overviewpage.h +++ b/src/qt/overviewpage.h @@ -20,7 +20,6 @@ #include #include - class ClientModel; class TransactionFilterProxy; class TxViewDelegate; @@ -70,6 +69,9 @@ public Q_SLOTS: void enabledTorChanged(); void outOfSyncWarningClicked(); void spatsRegistryChangedSignal(); + void gotoSendCoinsPage(); + void gotoReceiveCoinsPage(); + private: Ui::OverviewPage *ui; ClientModel *clientModel; @@ -89,7 +91,7 @@ public Q_SLOTS: std::unique_ptr filter; QTimer countDownTimer; - int secDelay; + int secDelay{30}; QString migrationWindowClosesIn; QString blocksRemaining; QString migrateAmount; @@ -100,7 +102,7 @@ public Q_SLOTS: void displaySpatsBalances(); const spats::SparkAssetDisplayAttributes* getSpatsDisplayAttributes(spats::universal_asset_id_t asset_id); void adjustTextSize(int width,int height); - + void addShadow(QWidget *w); void process_spats_registry_changed(const admin_addresses_set_t &affected_asset_admin_addresses, const asset_ids_set_t &affected_asset_ids) override; private Q_SLOTS: @@ -112,16 +114,14 @@ private Q_SLOTS: void handleOutOfSyncWarningClicks(); void countDown(); void handleSpatsRegistryChangedSignal(); - - void on_tableWidgetSparkBalances_contextMenuRequested(const QPoint &pos); }; class MigrateLelantusToSparkDialog : public QMessageBox { Q_OBJECT private: - bool clickedButton; - WalletModel *model; + bool clickedButton{false}; + WalletModel *model{nullptr}; public: MigrateLelantusToSparkDialog(WalletModel *model); bool getClickedButton(); diff --git a/src/qt/receivecoinsdialog.cpp b/src/qt/receivecoinsdialog.cpp index 22a71d3887..b3c6ff2fc0 100644 --- a/src/qt/receivecoinsdialog.cpp +++ b/src/qt/receivecoinsdialog.cpp @@ -26,6 +26,7 @@ #include #include #include +#include ReceiveCoinsDialog::ReceiveCoinsDialog(const PlatformStyle *_platformStyle, QWidget *parent) : QDialog(parent), @@ -88,8 +89,29 @@ ReceiveCoinsDialog::ReceiveCoinsDialog(const PlatformStyle *_platformStyle, QWid connect(ui->addressTypeCombobox, qOverload(&QComboBox::activated), this, &ReceiveCoinsDialog::displayCheckBox); connect(ui->createSparkNameButton, &QPushButton::clicked, this, &ReceiveCoinsDialog::createSparkName); + + addShadow(ui->frame); + addShadow(ui->frame2); + addShadow(ui->receiveButton); + addShadow(ui->clearButton); + addShadow(ui->createSparkNameButton); + addShadow(ui->showRequestButton); + addShadow(ui->removeRequestButton); + addShadow(ui->recentRequestsView); + addShadow(ui->addressTypeCombobox); + addShadow(ui->addressTypeHistoryCombobox); } +void ReceiveCoinsDialog::addShadow(QWidget* w) +{ + auto *shadow = new QGraphicsDropShadowEffect(this); + shadow->setBlurRadius(18); + shadow->setOffset(0, 4); + shadow->setColor(QColor(0, 0, 0, 60)); + w->setGraphicsEffect(shadow); +} + + void ReceiveCoinsDialog::setModel(WalletModel *_model) { this->model = _model; diff --git a/src/qt/receivecoinsdialog.h b/src/qt/receivecoinsdialog.h index 5d2ee4445c..3075b862f2 100644 --- a/src/qt/receivecoinsdialog.h +++ b/src/qt/receivecoinsdialog.h @@ -57,6 +57,7 @@ class ReceiveCoinsDialog : public QDialog explicit ReceiveCoinsDialog(const PlatformStyle *platformStyle, QWidget *parent = 0); ~ReceiveCoinsDialog(); + void addShadow(QWidget* w); void setModel(WalletModel *model); void resizeEvent(QResizeEvent* event) override; diff --git a/src/qt/res/css/firo.css b/src/qt/res/css/firo.css index 27afcf4324..8c91275788 100644 --- a/src/qt/res/css/firo.css +++ b/src/qt/res/css/firo.css @@ -1819,3 +1819,215 @@ QPushButton#warningIcon { background-color: #f4f4f4; height: 33px; } + +QWidget#OverviewPage { + background: #FFFFFF; + font-family: "Segoe UI"; + } + QFrame#card, QFrame#warningFrame, QFrame#sparkCard, QFrame#activityCard { + background: #FFFFFF; + border: none; + border-radius: 0; + margin: 0px; + padding: 0px; + } + QLabel.hint { + color: #6B7280; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.2px; + } + + QLabel#labelTotalText { + color: #111827; + font-weight: 700; + font-size: 30pt; + padding: 0px; + background: transparent; + } + QLabel#labelTotal { + color: #111827; + font-weight: 500; + font-size: 30pt; + padding: 0px; + background: transparent; + } + + QLabel#labelTransparentAvailableText, + QLabel#labelPendingText, + QLabel#labelTransparentImmatureText, + QLabel#labelPrivateAvailableText { + color: #6B7280; + font-size: 8px; + } + + QLabel#labelBalance, + QLabel#labelUnconfirmed, + QLabel#labelImmature, + QLabel#labelAnonymizable { + color: #111827; + font-size: 8px; + font-weight: 600; + } + + QLabel#labelAlerts { + background: #FFF7E6; + color: #92400E; + border-radius: 10px; + padding: 10px 12px; + border: 1px solid #FDEDD0; + } + + QPushButton#labelWalletStatus, + QPushButton#labelTransactionsStatus { + background: transparent; + border: none; + } + + QPushButton#accentButton, + QPushButton#migrateButton, + QPushButton#anonymizeButton { + color: #FFFFFF; + font-weight: 600; + border: none; + border-radius: 10px; + padding: 2px 7px; + background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #B24040, + stop:1 #B24040); + } + QPushButton#accentButton:hover, + QPushButton#migrateButton:hover, + QPushButton#anonymizeButton:hover { + background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #B24040, + stop:1 #B24040); + } + QPushButton#accentButton:disabled, + QPushButton#migrateButton:disabled, + QPushButton#anonymizeButton:disabled { + background: #CFCFCF; + color: #F9FAFB; + } + QPushButton#secondaryButton { + background: #E5E7EB; + color: #111827; + border: none; + border-radius: 8px; + padding: 2px 7px; + font-weight: 600; + } + QPushButton#secondaryButton:hover { + background: #D1D5DB; + } + + QTableWidget#tableWidgetSparkBalances { + background: #FFFFFF; + border-radius: 10px; + border: 1px solid #FFFFFF; + gridline-color: #EEF0F2; + selection-background-color: ##FFFFFF; + selection-color: #111827; + alternate-background-color: ##FFFFFF; + } + QHeaderView::section { + background: #F3F4F6; + color: #374151; + padding: 8px; + border: none; + border-right: 1px solid #E5E7EB; + font-weight: 600; + } + QTableCornerButton::section { background: #F3F4F6; border: none; } + QTableWidget::item { padding: 8px; } + + QListView#listTransactions { + background: transparent; + border: none; + padding: 0; + } + QCheckBox#checkboxEnabledTor { color: #374151; } + + QWidget#scrollWidgetSparkAssets { + background: #FFFFFF; + border: none; + padding: 0px; + } + + QWidget#scrollWidgetSparkAssets QLabel { + color: #000000; + font-size: 10pt; + font-weight: 500; + background: transparent; + border: none; + padding: 2px 0px; + } + + QWidget#scrollWidgetSparkAssets QLabel#balance { + color: #000000; + font-weight: 600; + font-size: 10pt; + } + + QWidget#scrollWidgetSparkAssets QFrame { + background: #FFFFFF; + border: none; + padding: 4px 0px; + } + + QWidget#scrollWidgetSparkAssets QFrame:hover { + background: #FFFFFF; + } + QLabel#labelHeaderAssetID, + QLabel#labelHeaderName, + QLabel#labelHeaderAvailable { + color: #000000; + font-size: 9pt; + font-weight: 600; + background: transparent; + } + QFrame#sparkCard, + QFrame#activityCard { + background: #FFFFFF; + border-radius: 18px; + border: 1px solid #F0F0F0; + padding: 8px; + } + + QFrame#balancesCard { + background: #FFFFFF; + border-radius: 15px; + border: 1px solidrgb(151, 149, 149); + padding: 8px; + } + + QFrame#framePrivateAvailable, + QFrame#framePrivatePending, + QFrame#frameTransparentAvailable, + QFrame#frameTransparentPending, + QFrame#frameTransparentImmature { + background:rgb(144, 146, 150); + border-radius: 14px; + padding: 4px 10px; + border: none; + } + + QFrame#framePrivateAvailable QLabel, + QFrame#framePrivatePending QLabel, + QFrame#frameTransparentAvailable QLabel, + QFrame#frameTransparentPending QLabel, + QFrame#frameTransparentImmature QLabel { + background: transparent; + font-size: 10pt; + font-weight: 600; + color: #111827; + } + + QLabel#labelPrivateAvailableTitle, + QLabel#labelPrivatePendingTitle, + QLabel#labelTransparentAvailableTitle, + QLabel#labelTransparentPendingTitle, + QLabel#labelTransparentImmatureTitle { + font-weight: 500; + color: #6B7280; + } diff --git a/src/qt/sendcoinsdialog.cpp b/src/qt/sendcoinsdialog.cpp index 4c0f31ae90..37a89ddde1 100644 --- a/src/qt/sendcoinsdialog.cpp +++ b/src/qt/sendcoinsdialog.cpp @@ -31,6 +31,8 @@ #include #include #include +#include +#include #define SEND_CONFIRM_DELAY 3 @@ -45,6 +47,11 @@ SendCoinsDialog::SendCoinsDialog(const PlatformStyle *_platformStyle, QWidget *p platformStyle(_platformStyle) { ui->setupUi(this); + ui->scrollArea->setStyleSheet("QScrollArea { background: transparent; border: none; }"); + ui->scrollArea->viewport()->setStyleSheet("background: transparent;"); + ui->scrollAreaWidgetContents->setStyleSheet( + "QWidget#scrollAreaWidgetContents { background: transparent; border: none; }" + ); if (!_platformStyle->getImagesOnButtons()) { ui->addButton->setIcon(QIcon()); @@ -90,7 +97,7 @@ SendCoinsDialog::SendCoinsDialog(const PlatformStyle *_platformStyle, QWidget *p ui->labelCoinControlChange->addAction(clipboardChangeAction); ui->frameCoinControl->setAutoFillBackground(true); - ui->scrollArea->setAutoFillBackground(true); + ui->scrollArea->setAutoFillBackground(false); ui->frameFee->setAutoFillBackground(true); { @@ -127,8 +134,28 @@ SendCoinsDialog::SendCoinsDialog(const PlatformStyle *_platformStyle, QWidget *p ui->customFee->setValue(settings.value("nTransactionFee").toLongLong()); ui->checkBoxMinimumFee->setChecked(settings.value("fPayOnlyMinFee").toBool()); minimizeFeeSection(settings.value("fFeeSectionMinimized").toBool()); + + addShadow(ui->sendButton); + addShadow(ui->clearButton); + addShadow(ui->addButton); + addShadow(ui->switchFundButton); + + addShadow(ui->frameCoinControl); + addShadow(ui->frameFee); + addShadow(ui->scrollArea); + +} + +void SendCoinsDialog::addShadow(QWidget* w) +{ + auto *shadow = new QGraphicsDropShadowEffect(this); + shadow->setBlurRadius(18); + shadow->setOffset(0, 4); + shadow->setColor(QColor(0, 0, 0, 60)); + w->setGraphicsEffect(shadow); } + void SendCoinsDialog::setClientModel(ClientModel *_clientModel) { this->clientModel = _clientModel; @@ -774,7 +801,6 @@ void SendCoinsDialog::removeEntry(SendCoinsEntry* entry) // If the last entry is about to be removed add an empty one if (ui->entries->count() == 1) addEntry(); - entry->deleteLater(); updateTabsAndLabels(); diff --git a/src/qt/sendcoinsdialog.h b/src/qt/sendcoinsdialog.h index 5915a3c5c3..a12f346cc6 100644 --- a/src/qt/sendcoinsdialog.h +++ b/src/qt/sendcoinsdialog.h @@ -76,6 +76,7 @@ public Q_SLOTS: void updateFeeMinimizedLabel(); void setAnonymizeMode(bool enableAnonymizeMode); void removeUnmatchedOutput(CCoinControl &coinControl); + void addShadow(QWidget* w); private Q_SLOTS: void on_sendButton_clicked(); diff --git a/src/qt/sendcoinsentry.cpp b/src/qt/sendcoinsentry.cpp index af68509a0f..4020a71c42 100644 --- a/src/qt/sendcoinsentry.cpp +++ b/src/qt/sendcoinsentry.cpp @@ -16,7 +16,8 @@ #include #include -#include +#include +#include SendCoinsEntry::SendCoinsEntry(const PlatformStyle *_platformStyle, QWidget *parent) : QStackedWidget(parent), @@ -62,6 +63,29 @@ SendCoinsEntry::SendCoinsEntry(const PlatformStyle *_platformStyle, QWidget *par ui->messageLabel->setVisible(false); ui->messageTextLabel->setVisible(false); ui->iconMessageWarning->setVisible(false); + { + auto *shadow = new QGraphicsDropShadowEffect(this); + shadow->setBlurRadius(18); + shadow->setOffset(0, 4); + shadow->setColor(QColor(0, 0, 0, 60)); + + ui->SendCoins->setGraphicsEffect(shadow); + } + + ui->SendCoins->setStyleSheet( + "QFrame#SendCoins {" + " background: #FFFFFF;" + " border-radius: 20px;" + " border: 1px solid #FFFFFF;" + " padding: 16px;" + "}" + ); + + this->setStyleSheet( + "QStackedWidget#SendCoinsEntry {" + " background: #FFFFFF;" + "}" + ); } SendCoinsEntry::~SendCoinsEntry() diff --git a/src/qt/sparkassetspage.cpp b/src/qt/sparkassetspage.cpp new file mode 100644 index 0000000000..3990aa4ab6 --- /dev/null +++ b/src/qt/sparkassetspage.cpp @@ -0,0 +1,952 @@ +#include "sparkassetspage.h" +#include "ui_sparkassetspage.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include "random.h" +#include "../spats/manager.hpp" + +#include "../spark/state.h" +#include "../spark/sparkwallet.h" +#include "../wallet/wallet.h" +#include "spatsburndialog.h" + +#include "walletmodel.h" +#include "sparkassetdialog.h" +#include "spatsmintdialog.h" +#include "spatsuserconfirmationdialog.h" + +#include +#include "../utils/math.hpp" + + +namespace { + +enum MyOwnSpatsColumns { + ColumnAssetType = 0, + ColumnIdentifier, + ColumnSymbol, + ColumnName, + ColumnDescription, + ColumnTotalSupply, + ColumnFungible, + ColumnResupplyable, + ColumnPrecision, + ColumnMetadata, + ColumnCount +}; + +} + +namespace spats { + +SparkAssetsPage::SparkAssetsPage(const PlatformStyle *platform_style, QWidget *parent) + : QWidget(parent) + , platform_style_(platform_style) + , ui(new Ui::SparkAssetsPage) +{ + ui->setupUi(this); + + + connect(ui->chkFungible, &QCheckBox::toggled, this, [this](bool fungible){ + ui->labelSupply->setVisible(fungible); + ui->editSupply->setVisible(fungible); + + ui->labelPrecision->setVisible(fungible); + ui->comboPrecision->setVisible(fungible); + + ui->labelResupply->setVisible(fungible); + ui->comboResupply->setVisible(fungible); + + ui->labelIdentifier->setVisible(!fungible); + ui->editIdentifier->setVisible(!fungible); + ui->btnAutoId->setVisible(!fungible); + ui->btnGenerateId->setVisible(!fungible); + }); + + ui->chkFungible->setChecked(true); + ui->chkFungible->toggled(true); + + ui->tableAssets->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + ui->tableAssets->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft); + ui->tableAssets->verticalHeader()->setVisible(false); + ui->tableAssets->setShowGrid(false); + ui->tableAssets->setSelectionMode(QAbstractItemView::SingleSelection); + ui->tableAssets->setSelectionBehavior(QAbstractItemView::SelectRows); + + ui->btnPortfolio->setCheckable(true); + ui->btnMyCreations->setCheckable(true); + ui->btnCreateAsset->setCheckable(true); + + connect(ui->btnPortfolio, &QPushButton::clicked, this, [this]() { + ui->stackedAssets->setCurrentWidget(ui->pagePortfolio); + ui->searchContainer->show(); + + ui->btnPortfolio->setChecked(true); + ui->btnMyCreations->setChecked(false); + ui->btnCreateAsset->setChecked(false); + + display_all_assets(); + }); + + + connect(ui->btnMyCreations, &QPushButton::clicked, this, [this]() { + ui->stackedAssets->setCurrentWidget(ui->pageMyCreations); + ui->searchContainer->hide(); + ui->btnPortfolio->setChecked(false); + ui->btnMyCreations->setChecked(true); + ui->btnCreateAsset->setChecked(false); + }); + + connect( + ui->tableAssets->selectionModel(), + &QItemSelectionModel::selectionChanged, + this, + &SparkAssetsPage::onAssetRowClicked + ); + + + connect(ui->btnCreateAsset, &QPushButton::clicked, this, [this]() { + ui->stackedAssets->setCurrentWidget(ui->pageCreateAsset); + ui->searchContainer->hide(); + ui->btnPortfolio->setChecked(false); + ui->btnMyCreations->setChecked(false); + ui->btnCreateAsset->setChecked(true); + }); + + connect(ui->searchAssets, &QLineEdit::textChanged, + this, &SparkAssetsPage::filterPortfolioTable); + + connect(ui->btnRefresh, &QPushButton::clicked, + this, &SparkAssetsPage::onRefreshButtonClicked); + + connect(ui->btnClear, &QPushButton::clicked, + this, &SparkAssetsPage::onClearCreateForm); + + + ui->tableMyCreated->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + ui->tableMyCreated->verticalHeader()->setVisible(false); + ui->tableActivity->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + ui->tableActivity->verticalHeader()->setVisible(false); + + ui->tableMyCreated->setSelectionBehavior(QAbstractItemView::SelectRows); + ui->tableMyCreated->setSelectionMode(QAbstractItemView::SingleSelection); + + setupMyCreatedTableColumns(); + + addShadow(ui->btnMint); + addShadow(ui->btnBurn); + addShadow(ui->btnMetadata); + addShadow(ui->btnResupply); + addShadow(ui->btnRevoke); + + addShadow(ui->frameMyCreated); + addShadow(ui->frameActivity); + + addShadow(ui->assetsCard); + addShadow(ui->detailsCard); + + addShadow(ui->btnSend); + addShadow(ui->btnReceive); + addShadow(ui->btnAddWatch); + addShadow(ui->btnRemove); + addShadow(ui->btnExport); + addShadow(ui->btnDetailsSend); + addShadow(ui->btnDetailsReceive); + addShadow(ui->btnCopy); + addShadow(ui->btnPortfolio); + addShadow(ui->btnMyCreations); + addShadow(ui->btnCreateAsset); + + addShadow(ui->btnAll); + addShadow(ui->btnHeld); + addShadow(ui->btnWatchOnly); + addShadow(ui->btnRefresh); + addShadow(ui->frameCreateAsset); + + connect(ui->btnDoCreate, &QPushButton::clicked, this, &SparkAssetsPage::onCreateButtonClicked); + connect(ui->btnMint, &QPushButton::clicked, this, &SparkAssetsPage::onMintButtonClicked); + connect(ui->btnMetadata, &QPushButton::clicked, this, &SparkAssetsPage::onModifyButtonClicked); + connect(ui->btnResupply, &QPushButton::clicked, this, &SparkAssetsPage::onModifyButtonClicked); + connect(ui->btnRevoke, &QPushButton::clicked, this, &SparkAssetsPage::onUnregisterButtonClicked); + connect(ui->btnBurn, &QPushButton::clicked, this, &SparkAssetsPage::onBurnButtonClicked); + + connect(ui->tableMyCreated->selectionModel(), + &QItemSelectionModel::selectionChanged, + this, + &SparkAssetsPage::updateButtonStates); + + connect(this, &SparkAssetsPage::displayMyOwnSpatsSignal, + this, &SparkAssetsPage::handleDisplayMyOwnSpatsSignal); + + connect(ui->btnAutoId, &QPushButton::clicked, this, [this]() { + ui->editIdentifier->setText("0"); + }); + + connect(ui->btnGenerateId, &QPushButton::clicked, this, [this]() { + uint64_t new_id = GetRand(UINT64_MAX); + + ui->editIdentifier->setText(QString::number(new_id)); + }); + ui->chkFungible->setChecked(true); + ui->chkFungible->toggled(true); + + updateButtonStates(); +} + +SparkAssetsPage::~SparkAssetsPage() +{ + spark::CSparkState::GetState()->GetSpatsManager().remove_updates_observer(*this); + delete ui; +} + +void SparkAssetsPage::onAssetRowClicked() +{ + int row = ui->tableAssets->currentRow(); + if (row < 0) + return; + + QString symbol = ui->tableAssets->item(row, 1)->text(); + + auto ®istry = spark::CSparkState::GetState()->GetSpatsManager().registry(); + + std::optional found; + + { + std::shared_lock lock(registry.mutex_); + + for (const auto &p : registry.fungible_assets_) { + spats::SparkAssetDisplayAttributes a(p.second); + if (QString::fromStdString(a.symbol) == symbol) { + found = p.second; + break; + } + } + + if (!found) { + for (const auto &line : registry.nft_lines_) { + for (const auto &kv : line.second) { + spats::SparkAssetDisplayAttributes a(kv.second); + if (QString::fromStdString(a.symbol) == symbol) { + found = kv.second; + break; + } + } + if (found) break; + } + } + } + if (!found) + return; + + showAssetDetails(spats::SparkAssetDisplayAttributes(*found)); +} + +void SparkAssetsPage::addShadow(QWidget *w) +{ + auto *shadow = new QGraphicsDropShadowEffect(this); + shadow->setBlurRadius(18); + shadow->setOffset(0, 4); + shadow->setColor(QColor(0, 0, 0, 60)); + w->setGraphicsEffect(shadow); +} + +void SparkAssetsPage::setupMyCreatedTableColumns() +{ + ui->tableMyCreated->setColumnCount(ColumnCount); + + QStringList headers; + headers << tr("Asset type") + << tr("Identifier") + << tr("Symbol") + << tr("Name") + << tr("Description") + << tr("Total supply") + << tr("Fungible") + << tr("Resupplyable") + << tr("Precision") + << tr("Metadata"); + + ui->tableMyCreated->setHorizontalHeaderLabels(headers); + ui->tableMyCreated->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); +} + +void SparkAssetsPage::setClientModel(ClientModel *model) +{ + client_model_ = model; +} + +void SparkAssetsPage::setWalletModel(WalletModel *model) +{ + wallet_model_ = model; + + if (model) { + spark::CSparkState::GetState()->GetSpatsManager().add_updates_observer(*this); + display_my_own_spats(); + updateAssetTypeField(); + display_all_assets(); + } +} + +void SparkAssetsPage::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + adjustTextSize(width(), height()); +} + +void SparkAssetsPage::adjustTextSize(int width, int height) +{ + const double font_size_scaling_factor = 70.0; + const int base_font_size = std::min(width, height) / font_size_scaling_factor; + const int font_size = std::min(15, std::max(12, base_font_size)); + QFont font = this->font(); + font.setPointSize(font_size); + + ui->labelMyAssetsTitle->setFont(font); + ui->labelActivityTitle->setFont(font); + ui->tableMyCreated->setFont(font); + ui->tableMyCreated->horizontalHeader()->setFont(font); + ui->tableMyCreated->verticalHeader()->setFont(font); +} + +void SparkAssetsPage::showAssetDetails(const spats::SparkAssetDisplayAttributes& d) +{ + QString txt; + + txt += "Name: " + QString::fromStdString(d.name) + "
"; + txt += "Symbol: " + QString::fromStdString(d.symbol) + "
"; + + if (!d.fungible) + txt += "Identifier: " + QString::number(d.identifier) + "
"; + + txt += "Type: " + QString(d.fungible ? "Fungible" : "NFT") + "
"; + txt += "Precision: " + QString::number(d.precision) + "
"; + + txt += "Total Supply: " + + QString::fromStdString(d.total_supply) + "
"; + + txt += "Resupplyable: " + QString(d.resupplyable ? "Yes" : "No") + "
"; + + txt += "
Description:
" + + QString::fromStdString(d.description) + "

"; + + txt += "Metadata (read-only)
"; + txt += "
" +
+           QString::fromStdString(d.metadata) +
+           "
"; + + ui->textDetails->setHtml(txt); + + ui->btnDetailsSend->setEnabled(true); + ui->btnDetailsReceive->setEnabled(true); + ui->btnCopy->setEnabled(true); +} + +void SparkAssetsPage::display_my_own_spats() +{ + if (!wallet_model_) + return; + + const auto &my_public_address = + wallet_model_->getWallet()->sparkWallet->getSpatsWallet().my_public_address_as_admin(); + + const auto my_own_assets = + spark::CSparkState::GetState()->GetSpatsManager() + .registry() + .get_assets_administered_by(my_public_address); + + my_own_assets_map_.clear(); + + auto &table_widget = *ui->tableMyCreated; + table_widget.clearContents(); + table_widget.setRowCount(static_cast(my_own_assets.size())); + + int row = 0; + for (const auto &asset : my_own_assets) { + const spats::SparkAssetDisplayAttributes a(asset); + + my_own_assets_map_.emplace( + spats::universal_asset_id_t{ spats::asset_type_t{ a.asset_type }, + spats::identifier_t{ a.identifier } }, + asset); + + table_widget.setItem(row, ColumnAssetType, + new QTableWidgetItem(QString::number(a.asset_type))); + table_widget.setItem(row, ColumnIdentifier, + new QTableWidgetItem(QString::number(a.identifier))); + table_widget.setItem(row, ColumnSymbol, + new QTableWidgetItem(QString::fromStdString(a.symbol))); + table_widget.setItem(row, ColumnName, + new QTableWidgetItem(QString::fromStdString(a.name))); + table_widget.setItem(row, ColumnDescription, + new QTableWidgetItem(QString::fromStdString(a.description))); + table_widget.setItem(row, ColumnTotalSupply, + new QTableWidgetItem(QString::fromStdString(a.total_supply))); + table_widget.setItem(row, ColumnFungible, + new QTableWidgetItem(a.fungible ? tr("Yes") : tr("No"))); + table_widget.setItem(row, ColumnResupplyable, + new QTableWidgetItem(a.resupplyable ? tr("Yes") : tr("No"))); + table_widget.setItem(row, ColumnPrecision, + new QTableWidgetItem(QString::number(a.precision))); + table_widget.setItem(row, ColumnMetadata, + new QTableWidgetItem(QString::fromStdString(a.metadata))); + + for (int col = ColumnAssetType; col < ColumnCount; ++col) { + auto *item = table_widget.item(row, col); + if (!item) continue; + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + } + + ++row; + } + + ui->labelMyAssetsTitle->setText( + tr("My Created Assets (%1)").arg(static_cast(my_own_assets.size()))); + + updateButtonStates(); +} + +NewSparkAssetCreationContext SparkAssetsPage::make_new_asset_creation_context() const +{ + const auto ®istry = spark::CSparkState::GetState()->GetSpatsManager().registry(); + const auto lowest_available_asset_type_for_new_fungible_asset = + registry.get_lowest_available_asset_type_for_new_fungible_asset(); + if (!lowest_available_asset_type_for_new_fungible_asset) [[unlikely]] + throw std::domain_error( + "No available fungible asset type values left, all possible values are taken!"); + const auto lowest_available_asset_type_for_new_nft_line = + registry.get_lowest_available_asset_type_for_new_nft_line(); + if (!lowest_available_asset_type_for_new_nft_line) [[unlikely]] + throw std::domain_error( + "No available NFT line asset type values left, all possible values are taken!"); + return NewSparkAssetCreationContext{ + wallet_model_->getWallet()->sparkWallet->getSpatsWallet().my_public_address_as_admin(), + utils::to_underlying(*lowest_available_asset_type_for_new_fungible_asset), + utils::to_underlying(*lowest_available_asset_type_for_new_nft_line) + }; +} + +static spats::supply_amount_t convert_to_supply_amount(double value, unsigned precision) +{ + const double scaled_value = + value * utils::math::integral_power(std::uintmax_t(10), precision); + + const spats::supply_amount_t a{ + boost::numeric_cast(std::round(scaled_value)), + precision + }; + + return a; +} + +void SparkAssetsPage::onCreateButtonClicked() +{ + assert(wallet_model_); + + try + { + const bool fungible = ui->chkFungible->isChecked(); + + QString name = ui->editName->text().trimmed(); + QString symbol = ui->editSymbol->text().trimmed(); + QString description = ui->editDescription->toPlainText().trimmed(); + QString metadata = ui->editMetadata->toPlainText().trimmed(); + QString supplyStr = ui->editSupply->text().trimmed(); + QString idStr = ui->editIdentifier->text().trimmed(); + QString typeStr = ui->editAssetType->text().trimmed(); + + if (name.isEmpty()) + throw std::runtime_error("Name cannot be empty."); + + if (symbol.isEmpty()) + throw std::runtime_error("Symbol cannot be empty."); + + if (typeStr.isEmpty()) + throw std::runtime_error("Asset type not assigned."); + + const uint64_t asset_type = typeStr.toULongLong(); + + uint64_t identifier = 0; + if (!fungible) + { + if (idStr.isEmpty()) + throw std::runtime_error("Identifier cannot be empty for NFT."); + + identifier = idStr.toULongLong(); + } + + double total_supply = 0; + unsigned precision = 0; + bool resupplyable = false; + + if (fungible) + { + if (supplyStr.isEmpty()) + throw std::runtime_error("Total supply cannot be empty."); + + total_supply = supplyStr.toDouble(); + precision = ui->comboPrecision->currentText().toUInt(); + resupplyable = (ui->comboResupply->currentText() == "Yes"); + } + + auto admin_addr = + wallet_model_->getWallet()->sparkWallet->getSpatsWallet().my_public_address_as_admin(); + + spats::AssetNaming naming( + spats::asset_name_t(name.toStdString()), + spats::asset_symbol_t(symbol.toStdString()), + description.toStdString() + ); + + std::optional created; + + if (fungible) + { + const auto supply_amount = + convert_to_supply_amount(total_supply, precision); + + spats::FungibleSparkAsset a( + spats::asset_type_t(asset_type), + naming, + metadata.toStdString(), + admin_addr, + supply_amount, + resupplyable + ); + + created = a; + } + else + { + spats::NonfungibleSparkAsset a( + spats::asset_type_t(asset_type), + spats::identifier_t(identifier), + naming, + metadata.toStdString(), + admin_addr + ); + + created = a; + } + + if (!created.has_value()) + throw std::runtime_error("Internal error: asset creation failed."); + + + wallet_model_->getWallet()->CreateNewSparkAsset( + *created, + admin_addr, + MakeSpatsUserConfirmationCallback(*wallet_model_, this) + ); + + QMessageBox::information(this, tr("Success"), + tr("Spark Asset successfully created.")); + + ui->editName->clear(); + ui->editSymbol->clear(); + ui->editDescription->clear(); + ui->editMetadata->clear(); + ui->editSupply->clear(); + ui->editIdentifier->clear(); + + display_my_own_spats(); + ui->btnMyCreations->click(); + } + catch (const std::exception &e) + { + QMessageBox::critical(this, tr("Error"), + tr("Failed to create asset:\n%1").arg(e.what())); + } +} + +void SparkAssetsPage::onMintButtonClicked() +{ + assert(wallet_model_); + if (const auto row = get_the_selected_row()) { + try { + const bool resupplyable = + ui->tableMyCreated->item(*row, ColumnResupplyable)->text() == tr("Yes"); + if (!resupplyable) + throw std::domain_error("Cannot mint for a non-resupplyable asset!"); + + const spats::asset_type_t asset_type{ + ui->tableMyCreated->item(*row, ColumnAssetType)->text().toULongLong() + }; + assert(is_fungible_asset_type(asset_type)); + const auto &asset = + my_own_assets_map_.at(spats::universal_asset_id_t{ asset_type, {} }); + const auto &fungible_asset = std::get(asset); + assert(fungible_asset.resupplyable()); + + SpatsMintDialog dialog(platform_style_, fungible_asset, this); + if (dialog.exec() == QDialog::Accepted) { + wallet_model_->getWallet()->MintSparkAssetSupply( + asset_type, + dialog.getNewSupply(), + dialog.getRecipient(), + nullptr, + MakeSpatsUserConfirmationCallback(*wallet_model_, this)); + } + } catch (const std::exception &e) { + QMessageBox::critical(this, tr("Error"), + tr("An error occurred: %1").arg(e.what())); + } + } else { + QMessageBox::critical(this, tr("Error"), + tr("Please select an asset to mint for.")); + } +} + +void SparkAssetsPage::onModifyButtonClicked() +{ + assert(wallet_model_); + if (const auto row = get_the_selected_row()) { + try { + const spats::asset_type_t asset_type{ + ui->tableMyCreated->item(*row, ColumnAssetType)->text().toULongLong() + }; + spats::identifier_t identifier{0}; + if (!is_fungible_asset_type(asset_type)) { + identifier = spats::identifier_t{ + ui->tableMyCreated->item(*row, ColumnIdentifier)->text().toULongLong() + }; + } + const auto &existing_asset = + my_own_assets_map_.at(spats::universal_asset_id_t{ asset_type, identifier }); + + SparkAssetDialog dialog(platform_style_, existing_asset, this); + if (dialog.exec() == QDialog::Accepted) { + wallet_model_->getWallet()->ModifySparkAsset( + existing_asset, + *dialog.getResultAsset(), + MakeSpatsUserConfirmationCallback(*wallet_model_, this)); + } + } catch (const std::exception &e) { + QMessageBox::critical(this, tr("Error"), + tr("An error occurred: %1").arg(e.what())); + } + } else { + QMessageBox::critical(this, tr("Error"), + tr("Please select an asset to modify.")); + } +} + +void SparkAssetsPage::onUnregisterButtonClicked() +{ + assert(wallet_model_); + if (const auto row = get_the_selected_row()) { + try { + const spats::asset_type_t asset_type{ + ui->tableMyCreated->item(*row, ColumnAssetType)->text().toULongLong() + }; + std::optional identifier; + + if (!is_fungible_asset_type(asset_type)) { + identifier = spats::identifier_t{ + ui->tableMyCreated->item(*row, ColumnIdentifier)->text().toULongLong() + }; + + if (any_other_nfts_within_same_line(asset_type, *identifier)) { + const QMessageBox::StandardButton reply = + QMessageBox::question( + this, + tr("Unregister NFT"), + tr("Would you like to unregister the whole NFT line or just this " + "specific NFT?"), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, + QMessageBox::Cancel); + + switch (reply) { + case QMessageBox::Yes: + identifier.reset(); + break; + case QMessageBox::No: + break; + case QMessageBox::Cancel: + default: + return; + } + } + } + + wallet_model_->getWallet()->UnregisterSparkAsset( + asset_type, + identifier, + MakeSpatsUserConfirmationCallback(*wallet_model_, this)); + } catch (const std::exception &e) { + QMessageBox::critical(this, tr("Error"), + tr("An error occurred: %1").arg(e.what())); + } + } else { + QMessageBox::critical(this, tr("Error"), + tr("Please select an asset to unregister.")); + } +} + +void SparkAssetsPage::onBurnButtonClicked() +{ + assert(wallet_model_); + + const auto row = get_the_selected_row(); + if (!row) { + QMessageBox::warning(this, tr("Error"), tr("Please select an asset to burn.")); + return; + } + + try { + const spats::asset_type_t asset_type{ + ui->tableMyCreated->item(*row, ColumnAssetType)->text().toULongLong() + }; + + if (!is_fungible_asset_type(asset_type)) { + throw std::domain_error("Burn is available only for fungible assets."); + } + + const auto &asset_variant = + my_own_assets_map_.at(spats::universal_asset_id_t{ asset_type, {} }); + + const auto &asset = std::get(asset_variant); + + const spats::supply_amount_t max_allowed = asset.total_supply(); + if (max_allowed == spats::supply_amount_t(0, asset.precision())) + throw std::domain_error("Cannot burn: supply is zero."); + + SpatsBurnDialog dialog( + platform_style_, + asset_type, + asset.naming().symbol.get(), + max_allowed, + this + ); + + if (dialog.exec() != QDialog::Accepted) + return; + + const auto burn_amount = dialog.getBurnAmount(); + const spats::asset_symbol_t& symbol = asset.naming().symbol; + + wallet_model_->getWallet()->BurnSparkAssetSupply( + asset_type, + asset.naming().symbol, + burn_amount, + MakeSpatsUserConfirmationCallback(*wallet_model_, this) + ); + + QMessageBox::information(this, tr("Success"), tr("Burn transaction sent.")); + } + catch (const std::exception &e) + { + QMessageBox::critical(this, tr("Error"), + tr("Failed to burn supply:\n%1").arg(e.what())); + } +} + +void SparkAssetsPage::updateButtonStates() +{ + const auto selected_row = get_the_selected_row(); + const bool row_selected = selected_row.has_value(); + + ui->btnMetadata->setEnabled(row_selected); + ui->btnResupply->setEnabled(row_selected); + ui->btnRevoke->setEnabled(row_selected); + + bool can_mint = false; + if (row_selected) { + auto *item = ui->tableMyCreated->item(*selected_row, ColumnResupplyable); + if (item && item->text() == tr("Yes")) + can_mint = true; + } + ui->btnMint->setEnabled(can_mint); +} + +std::optional SparkAssetsPage::get_the_selected_row() const +{ + const auto selection = ui->tableMyCreated->selectionModel()->selectedRows(); + return selection.size() == 1 + ? std::optional{selection.front().row()} + : std::optional{}; +} + +bool SparkAssetsPage::any_other_nfts_within_same_line(spats::asset_type_t asset_type, + spats::identifier_t identifier) const +{ + assert(!is_fungible_asset_type(asset_type)); + assert(asset_type <= spats::max_allowed_asset_type_value); + + return std::ranges::any_of( + my_own_assets_map_, + [&](const auto &kv) { + return kv.first.first == asset_type && kv.first.second != identifier; + }); +} + +void SparkAssetsPage::process_spats_registry_changed( + const admin_addresses_set_t &affected_asset_admin_addresses, + const asset_ids_set_t & /*affected_asset_ids*/) +{ + if (!wallet_model_) + return; + + const auto &my_public_address = + wallet_model_->getWallet()->sparkWallet->getSpatsWallet().my_public_address_as_admin(); + + if (std::ranges::any_of( + affected_asset_admin_addresses, + [&my_public_address](const auto &admin_address) { + return admin_address == my_public_address || admin_address.empty(); + })) { + Q_EMIT displayMyOwnSpatsSignal(); + } +} + +void SparkAssetsPage::updateAssetTypeField() +{ + if (!wallet_model_) + return; + + auto ctx = make_new_asset_creation_context(); + + if (ui->chkFungible->isChecked()) + ui->editAssetType->setText( + QString::number(ctx.lowest_available_asset_type_for_new_fungible_asset)); + else + ui->editAssetType->setText( + QString::number(ctx.lowest_available_asset_type_for_new_nft_line)); +} + +void SparkAssetsPage::display_all_assets() +{ + if (!wallet_model_) + return; + + const auto &balances = wallet_model_->getSpatsBalances(); + + auto ®istry = spark::CSparkState::GetState()->GetSpatsManager().registry(); + std::vector list; + + { + std::shared_lock lock(registry.mutex_); + for (const auto &p : registry.fungible_assets_) { + list.emplace_back(p.second); + } + + for (const auto &line : registry.nft_lines_) { + for (const auto &kv : line.second) { + list.emplace_back(kv.second); + } + } + } + + QTableWidget *table = ui->tableAssets; + if (!table) + return; + + const int COL_ID = 0; + const int COL_NAME = 1; + const int COL_AVAILABLE = 2; + + table->setColumnCount(3); + table->clearContents(); + table->setRowCount(static_cast(list.size())); + + table->setHorizontalHeaderLabels({ "Asset ID", "Name", "Available" }); + + int row = 0; + + for (const auto &a : list) + { + QString idText; + if (a.fungible) + idText = QString("%1:0").arg(a.asset_type); + else + idText = QString("%1:%2").arg(a.asset_type).arg(a.identifier); + + table->setItem(row, COL_ID, new QTableWidgetItem(idText)); + table->setItem( + row, COL_NAME, + new QTableWidgetItem(QString::fromStdString(a.name)) + ); + + spats::identifier_t ident = + a.fungible ? spats::identifier_t{} : spats::identifier_t{ a.identifier }; + + spats::universal_asset_id_t uid{ + spats::asset_type_t(a.asset_type), + ident + }; + + QString availableText = "0"; + + auto it = balances.find(uid); + if (it != balances.end()) { + // available — это CAmount + availableText = QString::number(it->second.available.raw()); + } + + table->setItem(row, COL_AVAILABLE, new QTableWidgetItem(availableText)); + + for (int col = 0; col < 3; col++) { + auto *it = table->item(row, col); + if (it) + it->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + } + + row++; + } + ui->labelAssets->setText(tr("Assets (%1)").arg(list.size())); +} + +void SparkAssetsPage::filterPortfolioTable(const QString &query) +{ + QString q = query.trimmed().toLower(); + + QTableWidget *table = ui->tableAssets; + if (!table) + return; + + for (int row = 0; row < table->rowCount(); ++row) { + bool match = false; + + if (table->item(row, 0)->text().toLower().contains(q)) + match = true; + + if (table->item(row, 1)->text().toLower().contains(q)) + match = true; + + if (table->item(row, 2)->text().toLower().contains(q)) + match = true; + + table->setRowHidden(row, !match); + } +} + +void SparkAssetsPage::onRefreshButtonClicked() +{ + display_all_assets(); + filterPortfolioTable(ui->searchAssets->text()); + updateButtonStates(); +} + +void SparkAssetsPage::onClearCreateForm() +{ + ui->editName->clear(); + ui->editSymbol->clear(); + ui->editDescription->clear(); + ui->editMetadata->clear(); + ui->editSupply->clear(); + ui->editIdentifier->clear(); + + ui->chkFungible->setChecked(true); + ui->comboPrecision->setCurrentIndex(0); + ui->comboResupply->setCurrentIndex(0); + updateAssetTypeField(); + ui->editName->setFocus(); +} + +} \ No newline at end of file diff --git a/src/qt/sparkassetspage.h b/src/qt/sparkassetspage.h new file mode 100644 index 0000000000..cc95a78164 --- /dev/null +++ b/src/qt/sparkassetspage.h @@ -0,0 +1,78 @@ +#ifndef SPARK_ASSET_PAGE_H +#define SPARK_ASSET_PAGE_H + +#include +#include +#include +#include + +#include "../spats/manager.hpp" +#include "platformstyle.h" + +namespace Ui { +class SparkAssetsPage; +} + +class ClientModel; +class WalletModel; +struct NewSparkAssetCreationContext; + +namespace spats { +class SparkAssetsPage : public QWidget, public spats::UpdatesObserver +{ + Q_OBJECT + +public: + explicit SparkAssetsPage(const PlatformStyle *platform_style, QWidget *parent = nullptr); + ~SparkAssetsPage() override; + void setClientModel(ClientModel *client_model); + void setWalletModel(WalletModel *wallet_model); + void adjustTextSize(int width, int height); + void display_all_assets(); + void filterPortfolioTable(const QString &query); + +protected: + void resizeEvent(QResizeEvent *event) override; + +private Q_SLOTS: + void onCreateButtonClicked(); + void onMintButtonClicked(); + void onModifyButtonClicked(); + void onUnregisterButtonClicked(); + void handleDisplayMyOwnSpatsSignal() { display_my_own_spats(); } + void updateButtonStates(); + void onAssetRowClicked(); + void onBurnButtonClicked(); + void onRefreshButtonClicked(); + void onClearCreateForm(); + +Q_SIGNALS: + void displayMyOwnSpatsSignal(); + +private: + void process_spats_registry_changed(const admin_addresses_set_t &affected_asset_admin_addresses, + const asset_ids_set_t &affected_asset_ids) override; + + void display_my_own_spats(); + NewSparkAssetCreationContext make_new_asset_creation_context() const; + std::optional get_the_selected_row() const; + bool any_other_nfts_within_same_line(spats::asset_type_t asset_type, + spats::identifier_t identifier) const; + + // === UI helpers === + void addShadow(QWidget *w); + void setupMyCreatedTableColumns(); + void updateAssetTypeField(); + + void showAssetDetails(const spats::SparkAssetDisplayAttributes& d); + void updateAssetDetails(const spats::SparkAssetDisplayAttributes& d); + +private: + const PlatformStyle *platform_style_; + Ui::SparkAssetsPage *ui; + ClientModel *client_model_{}; + WalletModel *wallet_model_{}; + std::map my_own_assets_map_; +}; +} +#endif // SPARK_ASSET_PAGE_H diff --git a/src/qt/transactionrecord.cpp b/src/qt/transactionrecord.cpp index 3263713623..c55d6e3aed 100644 --- a/src/qt/transactionrecord.cpp +++ b/src/qt/transactionrecord.cpp @@ -46,6 +46,44 @@ QList TransactionRecord::decomposeTransaction(const CWallet * uint256 hash = wtx.GetHash(); std::map mapValue = wtx.mapValue; + if (wtx.tx->IsSpatsTransaction()) { + TransactionRecord rec(hash, nTime); + rec.involvesWatchAddress = false; + rec.debit = nDebit; + rec.credit = nCredit; + + if (wtx.tx->IsSpatsCreate()) { + rec.type = TransactionRecord::SpatsCreate; + rec.address = "Spats Create"; + } + else if (wtx.tx->IsSpatsMint()) { + rec.type = TransactionRecord::SpatsMint; + rec.address = "Spats Mint"; + } + else if (wtx.tx->IsSpatsModify()) { + rec.type = TransactionRecord::SpatsModify; + rec.address = "Spats Modify"; + } + else if (wtx.tx->IsSpatsUnregister()) { + rec.type = TransactionRecord::SpatsRevoke; + rec.address = "Spats Unregister"; + } + else if (wtx.tx->IsSpatsBurn()) { + rec.type = TransactionRecord::SpatsRevoke; + rec.address = "Spats Burn"; + } + else if (wtx.tx->HasSpatsMintCoin()) { + rec.type = TransactionRecord::SpatsMint; + rec.address = "Spats MintCoin"; + } + else if (wtx.tx->HasSpatsBurnAmount()) { + rec.type = TransactionRecord::SpatsRevoke; + rec.address = "Spats BurnAmount"; + } + parts.append(rec); + return parts; + } + bool isAllSigmaSpendFromMe = false; if (wtx.tx->vin[0].IsSigmaSpend()) { isAllSigmaSpendFromMe = (wallet->IsMine(wtx.tx->vin[0], *wtx.tx) & ISMINE_SPENDABLE); diff --git a/src/qt/transactionrecord.h b/src/qt/transactionrecord.h index b8ff33a592..846527d858 100644 --- a/src/qt/transactionrecord.h +++ b/src/qt/transactionrecord.h @@ -95,7 +95,11 @@ class TransactionRecord SpendSparkToSelf, MintSparkTo, SpendSparkTo, - RecvSpark + RecvSpark, + SpatsCreate, + SpatsMint, + SpatsModify, + SpatsRevoke }; /** Number of confirmation recommended for accepting a transaction */ diff --git a/src/qt/transactionview.cpp b/src/qt/transactionview.cpp index 0759fb1692..c547fba1ca 100644 --- a/src/qt/transactionview.cpp +++ b/src/qt/transactionview.cpp @@ -33,6 +33,7 @@ #include #include #include +#include namespace { char const * CopyLabelText{"Copy label"}; @@ -40,20 +41,24 @@ char const * CopyRapText{"Copy RAP address/label"}; } TransactionView::TransactionView(const PlatformStyle *platformStyle, QWidget *parent) : - QWidget(parent), model(0), transactionProxyModel(0), - transactionView(0), abandonAction(0) + QWidget(parent), + model(0), + transactionProxyModel(0), + transactionView(0), + abandonAction(0) { - // Build filter row setContentsMargins(0,0,0,0); - headerLayout = new QHBoxLayout(); - headerLayout->setContentsMargins(0,0,0,0); + QFrame* filterCard = new QFrame(this); + filterCard->setObjectName("filterCard"); + + headerLayout = new QHBoxLayout(filterCard); + headerLayout->setContentsMargins(10,10,10,10); + headerLayout->setSpacing(10); if (platformStyle->getUseExtraSpacing()) { - headerLayout->setSpacing(5); headerLayout->addSpacing(26); } else { - headerLayout->setSpacing(0); headerLayout->addSpacing(23); } @@ -71,11 +76,7 @@ TransactionView::TransactionView(const PlatformStyle *platformStyle, QWidget *pa headerLayout->addWidget(instantsendWidget); dateWidget = new QComboBox(this); - if (platformStyle->getUseExtraSpacing()) { - dateWidget->setFixedWidth(121); - } else { - dateWidget->setFixedWidth(120); - } + dateWidget->setFixedWidth(120); dateWidget->addItem(tr("All"), All); dateWidget->addItem(tr("Today"), Today); dateWidget->addItem(tr("This week"), ThisWeek); @@ -86,17 +87,14 @@ TransactionView::TransactionView(const PlatformStyle *platformStyle, QWidget *pa headerLayout->addWidget(dateWidget); typeWidget = new QComboBox(this); - if (platformStyle->getUseExtraSpacing()) { - typeWidget->setFixedWidth(121); - } else { - typeWidget->setFixedWidth(120); - } - + typeWidget->setFixedWidth(120); typeWidget->addItem(tr("All"), TransactionFilterProxy::ALL_TYPES); - typeWidget->addItem(tr("Received with"), TransactionFilterProxy::TYPE(TransactionRecord::RecvWithAddress) | - TransactionFilterProxy::TYPE(TransactionRecord::RecvFromOther)); - typeWidget->addItem(tr("Sent to"), TransactionFilterProxy::TYPE(TransactionRecord::SendToAddress) | - TransactionFilterProxy::TYPE(TransactionRecord::SendToOther)); + typeWidget->addItem(tr("Received with"), + TransactionFilterProxy::TYPE(TransactionRecord::RecvWithAddress) | + TransactionFilterProxy::TYPE(TransactionRecord::RecvFromOther)); + typeWidget->addItem(tr("Sent to"), + TransactionFilterProxy::TYPE(TransactionRecord::SendToAddress) | + TransactionFilterProxy::TYPE(TransactionRecord::SendToOther)); typeWidget->addItem(tr("To yourself"), TransactionFilterProxy::TYPE(TransactionRecord::SendToSelf)); typeWidget->addItem(tr("Mined"), TransactionFilterProxy::TYPE(TransactionRecord::Generated)); typeWidget->addItem(tr("Other"), TransactionFilterProxy::TYPE(TransactionRecord::Other)); @@ -110,65 +108,60 @@ TransactionView::TransactionView(const PlatformStyle *platformStyle, QWidget *pa typeWidget->addItem(tr("Mint spark to"), TransactionFilterProxy::TYPE(TransactionRecord::MintSparkTo)); typeWidget->addItem(tr("Spend spark to"), TransactionFilterProxy::TYPE(TransactionRecord::SpendSparkTo)); typeWidget->addItem(tr("Received Spark"), TransactionFilterProxy::TYPE(TransactionRecord::RecvSpark)); - headerLayout->addWidget(typeWidget); addressWidget = new QLineEdit(this); -#if QT_VERSION >= 0x040700 addressWidget->setPlaceholderText(tr("Enter address or label to search")); -#endif headerLayout->addWidget(addressWidget); amountWidget = new QLineEdit(this); -#if QT_VERSION >= 0x040700 amountWidget->setPlaceholderText(tr("Min amount")); -#endif - if (platformStyle->getUseExtraSpacing()) { - amountWidget->setFixedWidth(97); - } else { - amountWidget->setFixedWidth(100); - } + amountWidget->setFixedWidth(100); amountWidget->setValidator(new QDoubleValidator(0, 1e20, 8, this)); headerLayout->addWidget(amountWidget); QVBoxLayout *vlayout = new QVBoxLayout(this); - vlayout->setContentsMargins(0,0,0,0); - vlayout->setSpacing(0); + vlayout->setContentsMargins(10,10,10,10); + vlayout->setSpacing(10); + + vlayout->addWidget(filterCard); + + dateRangeWidget = createDateRangeWidget(); + dateRangeWidget->setObjectName("dateRangeWidget"); + vlayout->addWidget(dateRangeWidget); + + QFrame* tableCard = new QFrame(this); + tableCard->setObjectName("tableCard"); + + QVBoxLayout* tableLayout = new QVBoxLayout(tableCard); + tableLayout->setContentsMargins(10,10,10,10); QTableView *view = new QTableView(this); - vlayout->addLayout(headerLayout); - vlayout->addWidget(createDateRangeWidget()); - vlayout->addWidget(view); - vlayout->setSpacing(0); - int width = view->verticalScrollBar()->sizeHint().width(); - // Cover scroll bar width with spacing - if (platformStyle->getUseExtraSpacing()) { - headerLayout->addSpacing(width+2); - } else { - headerLayout->addSpacing(width); - } - // Always show scroll bar + transactionView = view; + + tableLayout->addWidget(view); + vlayout->addWidget(tableCard); + view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); - view->setTabKeyNavigation(false); view->setContextMenuPolicy(Qt::CustomContextMenu); - view->setItemDelegateForColumn(TransactionTableModel::ToAddress, new GUIUtil::TextElideStyledItemDelegate(view)); - + view->setTabKeyNavigation(false); + view->setItemDelegateForColumn( + TransactionTableModel::ToAddress, + new GUIUtil::TextElideStyledItemDelegate(view) + ); view->installEventFilter(this); - transactionView = view; - - // Actions abandonAction = new QAction(tr("Abandon transaction"), this); - resendAction = new QAction(tr("Re-broadcast transaction"), this); + resendAction = new QAction(tr("Re-broadcast transaction"), this); - QAction *copyAddressAction = new QAction(tr("Copy address"), this); - copyLabelAction = new QAction(tr(CopyLabelText), this); - QAction *copyAmountAction = new QAction(tr("Copy amount"), this); - QAction *copyTxIDAction = new QAction(tr("Copy transaction ID"), this); - QAction *copyTxHexAction = new QAction(tr("Copy raw transaction"), this); - QAction *copyTxPlainText = new QAction(tr("Copy full transaction details"), this); - QAction *editLabelAction = new QAction(tr("Edit label"), this); - QAction *showDetailsAction = new QAction(tr("Show transaction details"), this); + QAction *copyAddressAction = new QAction(tr("Copy address"), this); + copyLabelAction = new QAction(tr(CopyLabelText), this); + QAction *copyAmountAction = new QAction(tr("Copy amount"), this); + QAction *copyTxIDAction = new QAction(tr("Copy transaction ID"), this); + QAction *copyTxHexAction = new QAction(tr("Copy raw transaction"), this); + QAction *copyTxPlainText = new QAction(tr("Copy full transaction details"), this); + QAction *editLabelAction = new QAction(tr("Edit label"), this); + QAction *showDetailsAction = new QAction(tr("Show transaction details"), this); contextMenu = new QMenu(this); contextMenu->addAction(copyAddressAction); @@ -183,30 +176,110 @@ TransactionView::TransactionView(const PlatformStyle *platformStyle, QWidget *pa contextMenu->addAction(editLabelAction); contextMenu->addAction(resendAction); - // Connect actions - - connect(dateWidget, qOverload(&QComboBox::activated), this, &TransactionView::chooseDate); - connect(typeWidget, qOverload(&QComboBox::activated), this, &TransactionView::chooseType); - connect(watchOnlyWidget, qOverload(&QComboBox::activated), this, &TransactionView::chooseWatchonly); - connect(instantsendWidget, qOverload(&QComboBox::activated), this, &TransactionView::chooseInstantSend); - connect(addressWidget, &QLineEdit::textChanged, this, &TransactionView::changedPrefix); - connect(amountWidget, &QLineEdit::textChanged, this, &TransactionView::changedAmount); + connect(dateWidget, qOverload(&QComboBox::activated), this, &TransactionView::chooseDate); + connect(typeWidget, qOverload(&QComboBox::activated), this, &TransactionView::chooseType); + connect(watchOnlyWidget, qOverload(&QComboBox::activated), this, &TransactionView::chooseWatchonly); + connect(instantsendWidget, qOverload(&QComboBox::activated), this, &TransactionView::chooseInstantSend); + connect(addressWidget, &QLineEdit::textChanged, this, &TransactionView::changedPrefix); + connect(amountWidget, &QLineEdit::textChanged, this, &TransactionView::changedAmount); connect(view, &QTableView::doubleClicked, this, &TransactionView::doubleClicked); connect(view, &QTableView::customContextMenuRequested, this, &TransactionView::contextualMenu); connect(view->horizontalHeader(), &QHeaderView::sectionResized, this, &TransactionView::updateHeaderSizes); - connect(abandonAction, &QAction::triggered, this, &TransactionView::abandonTx); - connect(copyAddressAction, &QAction::triggered, this, &TransactionView::copyAddress); - connect(copyLabelAction, &QAction::triggered, this, &TransactionView::copyLabel); - connect(copyAmountAction, &QAction::triggered, this, &TransactionView::copyAmount); - connect(copyTxIDAction, &QAction::triggered, this, &TransactionView::copyTxID); - connect(copyTxHexAction, &QAction::triggered, this, &TransactionView::copyTxHex); - connect(copyTxPlainText, &QAction::triggered, this, &TransactionView::copyTxPlainText); - connect(editLabelAction, &QAction::triggered, this, &TransactionView::editLabel); - connect(showDetailsAction, &QAction::triggered, this, &TransactionView::showDetails); - connect(this, &TransactionView::doubleClicked, this, &TransactionView::showDetails); - connect(resendAction, &QAction::triggered, this, &TransactionView::rebroadcastTx); + connect(abandonAction, &QAction::triggered, this, &TransactionView::abandonTx); + connect(copyAddressAction, &QAction::triggered, this, &TransactionView::copyAddress); + connect(copyLabelAction, &QAction::triggered, this, &TransactionView::copyLabel); + connect(copyAmountAction, &QAction::triggered, this, &TransactionView::copyAmount); + connect(copyTxIDAction, &QAction::triggered, this, &TransactionView::copyTxID); + connect(copyTxHexAction, &QAction::triggered, this, &TransactionView::copyTxHex); + connect(copyTxPlainText, &QAction::triggered, this, &TransactionView::copyTxPlainText); + connect(editLabelAction, &QAction::triggered, this, &TransactionView::editLabel); + connect(showDetailsAction, &QAction::triggered, this, &TransactionView::showDetails); + connect(resendAction, &QAction::triggered, this, &TransactionView::rebroadcastTx); + + setStyleSheet( + "QWidget { background: #F7F8FA; font-family: 'Segoe UI'; color: #111827; font-size: 11pt; }" + + "QFrame#filterCard, QFrame#tableCard, QFrame#dateRangeWidget {" + " background: #FFFFFF;" + " border-radius: 18px;" + " border: 1px solid #F3F4F6;" + "}" + + "QLabel { font-size: 11pt; color: #111827; background: transparent; }" + + "QLineEdit, QComboBox {" + " background: #FFFFFF;" + " border-radius: 10px;" + " border: 1px solid #D1D5DB;" + " padding: 6px 10px;" + " font-size: 10pt;" + " color: #111827;" + "}" + "QLineEdit:!focus { color: #6B7280; }" + "QLineEdit:focus, QComboBox:focus { border: 1px solid #9CA3AF; }" + + "QComboBox QAbstractItemView {" + " background: #FFFFFF;" + " border-radius: 10px;" + " border: 1px solid #D1D5DB;" + " selection-background-color: #F3F4F6;" + " padding: 4px;" + "}" + + "QComboBox::drop-down { border: none; width: 24px; }" + "QComboBox::down-arrow { width: 12px; height: 12px; image: url(:/icons/arrow_down); }" + + "QDateTimeEdit { background:#FFFFFF; border-radius:10px; border:1px solid #D1D5DB; padding:6px 10px; }" + + "QCalendarWidget QWidget { background:#FFFFFF; }" + "QCalendarWidget QAbstractItemView { selection-background-color:#E5E7EB; border:none; }" + "QCalendarWidget QToolButton { color:#111827; background:transparent; font-weight:bold; }" + + "QTableView {" + " background: #FFFFFF;" + " border-radius: 14px;" + " border: 1px solid #E5E7EB;" + " font-size: 10pt;" + " gridline-color: #FFFFFF;" + " selection-background-color: #F3F4F6;" + "}" + + "QHeaderView::section { background:#FFFFFF; padding:6px; border:none; font-weight:600; color:#6B7280; }" + "QTableView::item:selected { background:#F1F3F5; color:#111827; }" + + "QScrollBar:vertical { background:#F3F4F6; width:12px; border-radius:6px; }" + "QScrollBar::handle:vertical { background:#D1D5DB; border-radius:6px; }" + "QScrollBar::handle:vertical:hover { background:#9CA3AF; }" + "QScrollBar::add-line, QScrollBar::sub-line { width:0; height:0; }" + + "QMenu { background:#FFFFFF; border:1px solid #D1D5DB; padding:6px; font-size:10pt; }" + "QMenu::item:selected { background:#F3F4F6; color:#111827; }" + + "QPushButton {" + " background:#F1F3F5;" + " border-radius:10px;" + " border:1px solid #E5E7EB;" + " padding:6px 14px;" + " font-size:10pt;" + " font-weight:600;" + "}" + "QPushButton:hover { background:#E5E7EB; }" + ); + + addShadow(filterCard); + addShadow(tableCard); + addShadow(dateRangeWidget); +} + +void TransactionView::addShadow(QWidget* w) +{ + auto *shadow = new QGraphicsDropShadowEffect(this); + shadow->setBlurRadius(18); + shadow->setOffset(0, 4); + shadow->setColor(QColor(0, 0, 0, 60)); + w->setGraphicsEffect(shadow); } void TransactionView::setModel(WalletModel *_model) @@ -600,11 +673,21 @@ void TransactionView::openThirdPartyTxUrl(QString url) QWidget *TransactionView::createDateRangeWidget() { - dateRangeWidget = new QFrame(); - dateRangeWidget->setFrameStyle(QFrame::Panel | QFrame::Raised); - dateRangeWidget->setContentsMargins(1,1,1,1); - QHBoxLayout *layout = new QHBoxLayout(dateRangeWidget); - layout->setContentsMargins(0,0,0,0); + dateRangeWidget = new QWidget(this); + dateRangeWidget->setObjectName("dateRangeWidget"); + + QFrame* frame = new QFrame(dateRangeWidget); + frame->setObjectName("filterCard"); + frame->setContentsMargins(10, 10, 10, 10); + + QHBoxLayout* outer = new QHBoxLayout(dateRangeWidget); + outer->setContentsMargins(0, 0, 0, 0); + outer->addWidget(frame); + + QHBoxLayout* layout = new QHBoxLayout(frame); + layout->setContentsMargins(10, 10, 10, 10); + layout->setSpacing(10); + layout->addSpacing(23); layout->addWidget(new QLabel(tr("Range:"))); @@ -624,14 +707,13 @@ QWidget *TransactionView::createDateRangeWidget() layout->addWidget(dateTo); layout->addStretch(); - // Hide by default dateRangeWidget->setVisible(false); - - // Notify on change connect(dateFrom, &QDateTimeEdit::dateChanged, this, &TransactionView::dateRangeChanged); connect(dateTo, &QDateTimeEdit::dateChanged, this, &TransactionView::dateRangeChanged); updateCalendarWidgets(); + addShadow(frame); + return dateRangeWidget; } diff --git a/src/qt/transactionview.h b/src/qt/transactionview.h index 540e6252af..2190df8826 100644 --- a/src/qt/transactionview.h +++ b/src/qt/transactionview.h @@ -79,8 +79,7 @@ class TransactionView : public QWidget QLineEdit *amountWidget; QMenu *contextMenu; - - QFrame *dateRangeWidget; + QWidget* dateRangeWidget; QDateTimeEdit *dateFrom; QDateTimeEdit *dateTo; QAction *copyLabelAction; @@ -91,6 +90,7 @@ class TransactionView : public QWidget void updateCalendarWidgets(); bool eventFilter(QObject *obj, QEvent *event); + void addShadow(QWidget* w); private Q_SLOTS: void contextualMenu(const QPoint &); diff --git a/src/qt/walletframe.cpp b/src/qt/walletframe.cpp index 862c9e631f..db886c5c29 100644 --- a/src/qt/walletframe.cpp +++ b/src/qt/walletframe.cpp @@ -146,6 +146,14 @@ void WalletFrame::gotoMyOwnSpatsPage() i.value()->gotoMyOwnSpatsPage(); } +void WalletFrame::gotoSparkAssetsPage() +{ + WalletView *view = currentWalletView(); + if (!view) + return; + view->gotoSparkAssetsPage(); +} + void WalletFrame::gotoReceiveCoinsPage() { QMap::const_iterator i; diff --git a/src/qt/walletframe.h b/src/qt/walletframe.h index 8103a72623..4142f2056c 100644 --- a/src/qt/walletframe.h +++ b/src/qt/walletframe.h @@ -80,6 +80,7 @@ public Q_SLOTS: void gotoReceiveCoinsPage(); /** Switch to send coins page */ void gotoSendCoinsPage(QString addr = ""); + void gotoSparkAssetsPage(); /** Show Sign/Verify Message dialog and switch to sign message tab */ void gotoSignMessageTab(QString addr = ""); diff --git a/src/qt/walletview.cpp b/src/qt/walletview.cpp index 4b5253af91..62933b9b9c 100644 --- a/src/qt/walletview.cpp +++ b/src/qt/walletview.cpp @@ -22,6 +22,7 @@ #include "transactiontablemodel.h" #include "transactionview.h" #include "walletmodel.h" +#include "sparkassetspage.h" #include @@ -53,6 +54,7 @@ WalletView::WalletView(const PlatformStyle *_platformStyle, QWidget *parent): sendCoinsPage = new QWidget(this); masternodeListPage = new MasternodeList(platformStyle); myOwnSpatsPage = new MyOwnSpats(platformStyle); + sparkAssetsPage = new spats::SparkAssetsPage(platformStyle, this); automintSparkNotification = new AutomintSparkNotification(this); automintSparkNotification->setWindowModality(Qt::NonModal); @@ -66,9 +68,12 @@ WalletView::WalletView(const PlatformStyle *_platformStyle, QWidget *parent): addWidget(sendCoinsPage); addWidget(masternodeListPage); addWidget(myOwnSpatsPage); + addWidget(sparkAssetsPage); // Clicking on a transaction on the overview pre-selects the transaction on the transaction history page connect(overviewPage, &OverviewPage::transactionClicked, this, &WalletView::focusBitcoinHistoryTab); + connect(overviewPage, &OverviewPage::gotoSendCoinsPage, this, QOverload<>::of(&WalletView::gotoSendCoinsPage)); + connect(overviewPage, &OverviewPage::gotoReceiveCoinsPage, this, QOverload<>::of(&WalletView::gotoReceiveCoinsPage)); } WalletView::~WalletView() @@ -169,6 +174,7 @@ void WalletView::setWalletModel(WalletModel *_walletModel) firoTransactionList->setModel(_walletModel); overviewPage->setWalletModel(_walletModel); receiveCoinsPage->setModel(_walletModel); + sparkAssetsPage->setWalletModel(_walletModel); // TODO: fix this //sendCoinsPage->setModel(_walletModel); usedReceivingAddressesPage->setModel(_walletModel->getAddressTableModel()); @@ -264,12 +270,24 @@ void WalletView::gotoMyOwnSpatsPage() setCurrentWidget(myOwnSpatsPage); } +void WalletView::gotoSparkAssetsPage() +{ + setCurrentWidget(sparkAssetsPage); +} + void WalletView::gotoReceiveCoinsPage() { setCurrentWidget(receiveCoinsPage); + Q_EMIT signalShowReceiveTab(); +} + +void WalletView::gotoSendCoinsPage() +{ + setCurrentWidget(sendCoinsPage); + Q_EMIT signalShowSendTab(); } -void WalletView::gotoSendCoinsPage(QString addr) +void WalletView::gotoSendCoinsPage(const QString &addr) { setCurrentWidget(sendCoinsPage); diff --git a/src/qt/walletview.h b/src/qt/walletview.h index 5f3e9fa0dc..a0f84e566d 100644 --- a/src/qt/walletview.h +++ b/src/qt/walletview.h @@ -14,6 +14,7 @@ #include "amount.h" #include "masternodelist.h" #include "myownspats.h" +#include "sparkassetspage.h" #include @@ -85,6 +86,7 @@ class WalletView : public QStackedWidget QWidget *firoTransactionsView; MasternodeList *masternodeListPage; MyOwnSpats *myOwnSpatsPage; + spats::SparkAssetsPage* sparkAssetsPage; QProgressDialog *progressDialog; const PlatformStyle *platformStyle; @@ -107,7 +109,10 @@ public Q_SLOTS: /** Switch to receive coins page */ void gotoReceiveCoinsPage(); /** Switch to send coins page */ - void gotoSendCoinsPage(QString addr = ""); + void gotoSendCoinsPage(); + void gotoSendCoinsPage(const QString &addr); + + void gotoSparkAssetsPage(); /** Show Sign/Verify Message dialog and switch to sign message tab */ void gotoSignMessageTab(QString addr = ""); @@ -176,6 +181,8 @@ public Q_SLOTS: void incomingTransaction(const QString& date, int unit, const CAmount& amount, const QString& type, const QString& address, const QString& label); /** Notify that the out of sync warning icon has been pressed */ void outOfSyncWarningClicked(); + void signalShowSendTab(); + void signalShowReceiveTab(); }; #endif // BITCOIN_QT_WALLETVIEW_H diff --git a/src/spats/registry.hpp b/src/spats/registry.hpp index f145862740..79dd0b2b8c 100644 --- a/src/spats/registry.hpp +++ b/src/spats/registry.hpp @@ -20,8 +20,11 @@ namespace spats { +class SparkAssetsPage; + class Registry { public: + friend class SparkAssetsPage; using block_hash_t = uint256; Registry();