feat: first working version

This commit is contained in:
Daniil Polyakov 2026-04-02 00:19:02 +03:00
parent e005abeee2
commit 871765de2d
24 changed files with 1690 additions and 25 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
result

View File

@ -26,6 +26,26 @@ set(SOURCES
src/ExplorerPlugin.h
src/ExplorerWidget.cpp
src/ExplorerWidget.h
src/Style.h
src/services/MockIndexerService.cpp
src/services/MockIndexerService.h
src/services/IndexerService.h
src/models/Block.h
src/models/Transaction.h
src/models/Account.h
src/widgets/SearchBar.cpp
src/widgets/SearchBar.h
src/widgets/NavigationBar.cpp
src/widgets/NavigationBar.h
src/widgets/ClickableFrame.h
src/pages/MainPage.cpp
src/pages/MainPage.h
src/pages/BlockPage.cpp
src/pages/BlockPage.h
src/pages/TransactionPage.cpp
src/pages/TransactionPage.h
src/pages/AccountPage.cpp
src/pages/AccountPage.h
)
# Create the plugin library

View File

@ -1,38 +1,197 @@
#include "ExplorerWidget.h"
#include "services/MockIndexerService.h"
#include "widgets/NavigationBar.h"
#include "widgets/SearchBar.h"
#include "pages/MainPage.h"
#include "pages/BlockPage.h"
#include "pages/TransactionPage.h"
#include "pages/AccountPage.h"
#include <QPainter>
#include <QVBoxLayout>
#include <QStackedWidget>
#include <QLabel>
ExplorerWidget::ExplorerWidget(QWidget* parent)
: QWidget(parent)
, m_indexer(std::make_unique<MockIndexerService>())
{
auto* layout = new QVBoxLayout(this);
layout->setContentsMargins(8, 8, 8, 8);
layout->setSpacing(4);
auto* label = new QLabel("LEZ Explorer", this);
label->setAlignment(Qt::AlignCenter);
QFont font = label->font();
font.setPointSize(24);
font.setBold(true);
label->setFont(font);
label->setStyleSheet("color: white;");
// Navigation bar
m_navBar = new NavigationBar(this);
layout->addWidget(m_navBar);
layout->addWidget(label);
// Search bar
m_searchBar = new SearchBar(this);
layout->addWidget(m_searchBar);
// Page stack
m_stack = new QStackedWidget(this);
layout->addWidget(m_stack, 1);
// Create main page
m_mainPage = new MainPage(m_indexer.get(), this);
m_stack->addWidget(m_mainPage);
connectPageSignals(m_mainPage);
m_currentTarget = NavHome{};
// Connect navigation
connect(m_navBar, &NavigationBar::backClicked, this, &ExplorerWidget::navigateBack);
connect(m_navBar, &NavigationBar::forwardClicked, this, &ExplorerWidget::navigateForward);
connect(m_navBar, &NavigationBar::homeClicked, this, &ExplorerWidget::navigateHome);
connect(m_searchBar, &SearchBar::searchRequested, this, &ExplorerWidget::onSearch);
updateNavButtons();
}
void ExplorerWidget::paintEvent(QPaintEvent* /*event*/)
ExplorerWidget::~ExplorerWidget() = default;
void ExplorerWidget::connectPageSignals(QWidget* page)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
// Draw a blue rectangle filling the widget
painter.setBrush(QColor(30, 60, 120));
painter.setPen(Qt::NoPen);
painter.drawRect(rect());
// Draw a lighter inner rectangle
int margin = 40;
QRect inner = rect().adjusted(margin, margin, -margin, -margin);
painter.setBrush(QColor(40, 80, 160));
painter.drawRoundedRect(inner, 16, 16);
if (auto* mainPage = qobject_cast<MainPage*>(page)) {
connect(mainPage, &MainPage::blockClicked, this, [this](quint64 id) {
navigateTo(NavBlock{id});
});
connect(mainPage, &MainPage::transactionClicked, this, [this](const QString& hash) {
navigateTo(NavTransaction{hash});
});
connect(mainPage, &MainPage::accountClicked, this, [this](const QString& id) {
navigateTo(NavAccount{id});
});
} else if (auto* blockPage = qobject_cast<BlockPage*>(page)) {
connect(blockPage, &BlockPage::blockClicked, this, [this](quint64 id) {
navigateTo(NavBlock{id});
});
connect(blockPage, &BlockPage::transactionClicked, this, [this](const QString& hash) {
navigateTo(NavTransaction{hash});
});
} else if (auto* txPage = qobject_cast<TransactionPage*>(page)) {
connect(txPage, &TransactionPage::accountClicked, this, [this](const QString& id) {
navigateTo(NavAccount{id});
});
} else if (auto* accPage = qobject_cast<AccountPage*>(page)) {
connect(accPage, &AccountPage::transactionClicked, this, [this](const QString& hash) {
navigateTo(NavTransaction{hash});
});
}
}
void ExplorerWidget::showPage(QWidget* page)
{
// Remove all pages except main page
while (m_stack->count() > 1) {
auto* w = m_stack->widget(1);
m_stack->removeWidget(w);
w->deleteLater();
}
if (page == m_mainPage) {
m_stack->setCurrentWidget(m_mainPage);
} else {
m_stack->addWidget(page);
m_stack->setCurrentWidget(page);
}
}
void ExplorerWidget::navigateTo(const NavTarget& target, bool addToHistory)
{
if (addToHistory) {
m_backHistory.push(m_currentTarget);
m_forwardHistory.clear();
}
m_currentTarget = target;
std::visit([this](auto&& t) {
using T = std::decay_t<decltype(t)>;
if constexpr (std::is_same_v<T, NavHome>) {
m_mainPage->clearSearchResults();
m_mainPage->refresh();
showPage(m_mainPage);
} else if constexpr (std::is_same_v<T, NavBlock>) {
auto block = m_indexer->getBlockById(t.blockId);
if (block) {
auto* page = new BlockPage(*block);
connectPageSignals(page);
showPage(page);
}
} else if constexpr (std::is_same_v<T, NavTransaction>) {
auto tx = m_indexer->getTransaction(t.hash);
if (tx) {
auto* page = new TransactionPage(*tx);
connectPageSignals(page);
showPage(page);
}
} else if constexpr (std::is_same_v<T, NavAccount>) {
auto account = m_indexer->getAccount(t.accountId);
if (account) {
auto* page = new AccountPage(*account, m_indexer.get());
connectPageSignals(page);
showPage(page);
}
}
}, target);
updateNavButtons();
}
void ExplorerWidget::navigateBack()
{
if (m_backHistory.isEmpty()) return;
m_forwardHistory.push(m_currentTarget);
NavTarget target = m_backHistory.pop();
navigateTo(target, false);
}
void ExplorerWidget::navigateForward()
{
if (m_forwardHistory.isEmpty()) return;
m_backHistory.push(m_currentTarget);
NavTarget target = m_forwardHistory.pop();
navigateTo(target, false);
}
void ExplorerWidget::navigateHome()
{
navigateTo(NavHome{});
}
void ExplorerWidget::onSearch(const QString& query)
{
if (query.trimmed().isEmpty()) return;
auto results = m_indexer->search(query);
// If exactly one result, navigate directly to it
int totalResults = results.blocks.size() + results.transactions.size() + results.accounts.size();
if (totalResults == 1) {
if (!results.blocks.isEmpty()) {
navigateTo(NavBlock{results.blocks.first().blockId});
return;
}
if (!results.transactions.isEmpty()) {
navigateTo(NavTransaction{results.transactions.first().hash});
return;
}
if (!results.accounts.isEmpty()) {
navigateTo(NavAccount{results.accounts.first().accountId});
return;
}
}
// Show results on main page
navigateTo(NavHome{});
m_mainPage->showSearchResults(results);
}
void ExplorerWidget::updateNavButtons()
{
m_navBar->setBackEnabled(!m_backHistory.isEmpty());
m_navBar->setForwardEnabled(!m_forwardHistory.isEmpty());
}

View File

@ -1,13 +1,49 @@
#pragma once
#include "services/IndexerService.h"
#include <QWidget>
#include <QStack>
#include <memory>
#include <variant>
class QStackedWidget;
class NavigationBar;
class SearchBar;
class MainPage;
struct NavHome {};
struct NavBlock { quint64 blockId; };
struct NavTransaction { QString hash; };
struct NavAccount { QString accountId; };
using NavTarget = std::variant<NavHome, NavBlock, NavTransaction, NavAccount>;
class ExplorerWidget : public QWidget {
Q_OBJECT
public:
explicit ExplorerWidget(QWidget* parent = nullptr);
~ExplorerWidget() override;
protected:
void paintEvent(QPaintEvent* event) override;
private:
void navigateTo(const NavTarget& target, bool addToHistory = true);
void navigateBack();
void navigateForward();
void navigateHome();
void onSearch(const QString& query);
void updateNavButtons();
void showPage(QWidget* page);
void connectPageSignals(QWidget* page);
std::unique_ptr<IndexerService> m_indexer;
NavigationBar* m_navBar = nullptr;
SearchBar* m_searchBar = nullptr;
QStackedWidget* m_stack = nullptr;
MainPage* m_mainPage = nullptr;
QStack<NavTarget> m_backHistory;
QStack<NavTarget> m_forwardHistory;
NavTarget m_currentTarget;
};

44
src/Style.h Normal file
View File

@ -0,0 +1,44 @@
#pragma once
#include <QString>
namespace Style {
inline QString cardFrame()
{
return "background: #f8f9fa; color: #212529; border: 1px solid #dee2e6; border-radius: 6px; padding: 12px;";
}
inline QString cardFrameWithLabels()
{
return "QFrame { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px; padding: 12px; }"
" QFrame QLabel { color: #212529; }";
}
inline QString clickableRow()
{
return "background: #f8f9fa; color: #212529; border: 1px solid #dee2e6; border-radius: 6px; padding: 8px; margin: 2px 0;";
}
inline QString clickableRowWithLabels(const QString& selector)
{
return QString("%1 { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px; padding: 8px; margin: 2px 0; }"
" %1 QLabel { color: #212529; }").arg(selector);
}
inline QString monoText()
{
return "font-family: 'Menlo', 'Courier New', 'DejaVu Sans Mono'; font-size: 11px;";
}
inline QString mutedText()
{
return "color: #6c757d;";
}
inline QString badge(const QString& bgColor)
{
return QString("color: white; background: %1; border-radius: 4px; padding: 2px 8px;").arg(bgColor);
}
} // namespace Style

11
src/models/Account.h Normal file
View File

@ -0,0 +1,11 @@
#pragma once
#include <QString>
struct Account {
QString accountId;
QString programOwner;
QString balance;
QString nonce;
int dataSizeBytes = 0;
};

34
src/models/Block.h Normal file
View File

@ -0,0 +1,34 @@
#pragma once
#include <QString>
#include <QVector>
#include <QDateTime>
#include "Transaction.h"
enum class BedrockStatus {
Pending,
Safe,
Finalized
};
inline QString bedrockStatusToString(BedrockStatus status)
{
switch (status) {
case BedrockStatus::Pending: return "Pending";
case BedrockStatus::Safe: return "Safe";
case BedrockStatus::Finalized: return "Finalized";
}
return "Unknown";
}
struct Block {
quint64 blockId = 0;
QString hash;
QString prevBlockHash;
QDateTime timestamp;
QString signature;
QVector<Transaction> transactions;
BedrockStatus bedrockStatus = BedrockStatus::Pending;
QString bedrockParentId;
};

47
src/models/Transaction.h Normal file
View File

@ -0,0 +1,47 @@
#pragma once
#include <QString>
#include <QVector>
enum class TransactionType {
Public,
PrivacyPreserving,
ProgramDeployment
};
inline QString transactionTypeToString(TransactionType type)
{
switch (type) {
case TransactionType::Public: return "Public";
case TransactionType::PrivacyPreserving: return "Privacy-Preserving";
case TransactionType::ProgramDeployment: return "Program Deployment";
}
return "Unknown";
}
struct AccountRef {
QString accountId;
QString nonce;
};
struct Transaction {
QString hash;
TransactionType type = TransactionType::Public;
// Public transaction fields
QString programId;
QVector<AccountRef> accounts;
QVector<quint32> instructionData;
int signatureCount = 0;
int proofSizeBytes = 0;
// Privacy-preserving transaction fields
int newCommitmentsCount = 0;
int nullifiersCount = 0;
int encryptedStatesCount = 0;
quint64 validityWindowStart = 0;
quint64 validityWindowEnd = 0;
// Program deployment fields
int bytecodeSizeBytes = 0;
};

134
src/pages/AccountPage.cpp Normal file
View File

@ -0,0 +1,134 @@
#include "AccountPage.h"
#include "Style.h"
#include "widgets/ClickableFrame.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QScrollArea>
#include <QFrame>
#include <QGridLayout>
namespace {
QLabel* makeFieldLabel(const QString& text)
{
auto* label = new QLabel(text);
label->setStyleSheet(Style::mutedText() + " font-weight: bold;");
return label;
}
QLabel* makeValueLabel(const QString& text)
{
auto* label = new QLabel(text);
label->setTextInteractionFlags(Qt::TextSelectableByMouse);
label->setWordWrap(true);
return label;
}
} // namespace
AccountPage::AccountPage(const Account& account, IndexerService* indexer, QWidget* parent)
: QWidget(parent)
{
auto* outerLayout = new QVBoxLayout(this);
auto* scrollArea = new QScrollArea(this);
scrollArea->setWidgetResizable(true);
scrollArea->setFrameShape(QFrame::NoFrame);
auto* scrollContent = new QWidget();
auto* layout = new QVBoxLayout(scrollContent);
layout->setAlignment(Qt::AlignTop);
// Title
auto* title = new QLabel("Account Details");
QFont titleFont = title->font();
titleFont.setPointSize(20);
titleFont.setBold(true);
title->setFont(titleFont);
layout->addWidget(title);
// Account info grid
auto* infoFrame = new QFrame();
infoFrame->setFrameShape(QFrame::StyledPanel);
infoFrame->setStyleSheet(Style::cardFrameWithLabels());
auto* grid = new QGridLayout(infoFrame);
grid->setColumnStretch(1, 1);
int row = 0;
grid->addWidget(makeFieldLabel("Account ID"), row, 0);
auto* idVal = makeValueLabel(account.accountId);
idVal->setStyleSheet(Style::monoText());
grid->addWidget(idVal, row++, 1);
grid->addWidget(makeFieldLabel("Balance"), row, 0);
grid->addWidget(makeValueLabel(account.balance), row++, 1);
grid->addWidget(makeFieldLabel("Program Owner"), row, 0);
auto* ownerVal = makeValueLabel(account.programOwner);
ownerVal->setStyleSheet(Style::monoText());
grid->addWidget(ownerVal, row++, 1);
grid->addWidget(makeFieldLabel("Nonce"), row, 0);
grid->addWidget(makeValueLabel(account.nonce), row++, 1);
grid->addWidget(makeFieldLabel("Data Size"), row, 0);
grid->addWidget(makeValueLabel(QString("%1 bytes").arg(account.dataSizeBytes)), row++, 1);
layout->addWidget(infoFrame);
// Transaction history
auto transactions = indexer->getTransactionsByAccount(account.accountId, 0, 10);
if (!transactions.isEmpty()) {
auto* txHeader = new QLabel("Transaction History");
QFont headerFont = txHeader->font();
headerFont.setPointSize(16);
headerFont.setBold(true);
txHeader->setFont(headerFont);
txHeader->setStyleSheet("margin-top: 16px; margin-bottom: 4px;");
layout->addWidget(txHeader);
for (const auto& tx : transactions) {
auto* frame = new ClickableFrame();
frame->setFrameShape(QFrame::StyledPanel);
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
auto* txRow = new QHBoxLayout(frame);
auto* hashLabel = new QLabel(tx.hash.left(16) + "...");
QFont boldFont = hashLabel->font();
boldFont.setBold(true);
hashLabel->setFont(boldFont);
QString typeColor;
switch (tx.type) {
case TransactionType::Public: typeColor = "#007bff"; break;
case TransactionType::PrivacyPreserving: typeColor = "#6f42c1"; break;
case TransactionType::ProgramDeployment: typeColor = "#fd7e14"; break;
}
auto* typeLabel = new QLabel(transactionTypeToString(tx.type));
typeLabel->setStyleSheet(Style::badge(typeColor));
txRow->addWidget(hashLabel);
txRow->addWidget(typeLabel);
txRow->addStretch();
QString txHash = tx.hash;
connect(frame, &ClickableFrame::clicked, this, [this, txHash]() {
emit transactionClicked(txHash);
});
layout->addWidget(frame);
}
} else {
auto* noTx = new QLabel("No transactions found for this account.");
noTx->setStyleSheet(Style::mutedText() + " margin-top: 16px;");
layout->addWidget(noTx);
}
scrollArea->setWidget(scrollContent);
outerLayout->addWidget(scrollArea);
}

16
src/pages/AccountPage.h Normal file
View File

@ -0,0 +1,16 @@
#pragma once
#include "models/Account.h"
#include "services/IndexerService.h"
#include <QWidget>
class AccountPage : public QWidget {
Q_OBJECT
public:
explicit AccountPage(const Account& account, IndexerService* indexer, QWidget* parent = nullptr);
signals:
void transactionClicked(const QString& hash);
};

162
src/pages/BlockPage.cpp Normal file
View File

@ -0,0 +1,162 @@
#include "BlockPage.h"
#include "Style.h"
#include "widgets/ClickableFrame.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QScrollArea>
#include <QFrame>
#include <QGridLayout>
namespace {
QLabel* makeFieldLabel(const QString& text)
{
auto* label = new QLabel(text);
label->setStyleSheet(Style::mutedText() + " font-weight: bold;");
return label;
}
QLabel* makeValueLabel(const QString& text)
{
auto* label = new QLabel(text);
label->setTextInteractionFlags(Qt::TextSelectableByMouse);
label->setWordWrap(true);
return label;
}
} // namespace
BlockPage::BlockPage(const Block& block, QWidget* parent)
: QWidget(parent)
{
auto* outerLayout = new QVBoxLayout(this);
auto* scrollArea = new QScrollArea(this);
scrollArea->setWidgetResizable(true);
scrollArea->setFrameShape(QFrame::NoFrame);
auto* scrollContent = new QWidget();
auto* layout = new QVBoxLayout(scrollContent);
layout->setAlignment(Qt::AlignTop);
// Title
auto* title = new QLabel(QString("Block #%1").arg(block.blockId));
QFont titleFont = title->font();
titleFont.setPointSize(20);
titleFont.setBold(true);
title->setFont(titleFont);
layout->addWidget(title);
// Block info grid
auto* infoFrame = new QFrame();
infoFrame->setFrameShape(QFrame::StyledPanel);
infoFrame->setStyleSheet(Style::cardFrameWithLabels());
auto* grid = new QGridLayout(infoFrame);
grid->setColumnStretch(1, 1);
int row = 0;
grid->addWidget(makeFieldLabel("Block ID"), row, 0);
grid->addWidget(makeValueLabel(QString::number(block.blockId)), row++, 1);
grid->addWidget(makeFieldLabel("Hash"), row, 0);
grid->addWidget(makeValueLabel(block.hash), row++, 1);
grid->addWidget(makeFieldLabel("Previous Hash"), row, 0);
auto* prevHashLabel = new QLabel(QString("<a href='#' style='color: #007bff;'>%1</a>").arg(block.prevBlockHash));
prevHashLabel->setTextInteractionFlags(Qt::TextBrowserInteraction);
if (block.blockId > 1) {
quint64 prevBlockId = block.blockId - 1;
connect(prevHashLabel, &QLabel::linkActivated, this, [this, prevBlockId]() {
emit blockClicked(prevBlockId);
});
}
grid->addWidget(prevHashLabel, row++, 1);
grid->addWidget(makeFieldLabel("Timestamp"), row, 0);
grid->addWidget(makeValueLabel(block.timestamp.toString("yyyy-MM-dd hh:mm:ss UTC")), row++, 1);
grid->addWidget(makeFieldLabel("Status"), row, 0);
QString statusColor;
switch (block.bedrockStatus) {
case BedrockStatus::Finalized: statusColor = "#28a745"; break;
case BedrockStatus::Safe: statusColor = "#ffc107"; break;
case BedrockStatus::Pending: statusColor = "#6c757d"; break;
}
auto* statusLabel = new QLabel(bedrockStatusToString(block.bedrockStatus));
statusLabel->setStyleSheet(Style::badge(statusColor) + " max-width: 100px;");
grid->addWidget(statusLabel, row++, 1);
grid->addWidget(makeFieldLabel("Signature"), row, 0);
auto* sigLabel = makeValueLabel(block.signature);
sigLabel->setStyleSheet(Style::monoText());
grid->addWidget(sigLabel, row++, 1);
grid->addWidget(makeFieldLabel("Transactions"), row, 0);
grid->addWidget(makeValueLabel(QString::number(block.transactions.size())), row++, 1);
layout->addWidget(infoFrame);
// Transactions section
if (!block.transactions.isEmpty()) {
auto* txHeader = new QLabel("Transactions");
QFont headerFont = txHeader->font();
headerFont.setPointSize(16);
headerFont.setBold(true);
txHeader->setFont(headerFont);
txHeader->setStyleSheet("margin-top: 16px; margin-bottom: 4px;");
layout->addWidget(txHeader);
for (const auto& tx : block.transactions) {
auto* frame = new ClickableFrame();
frame->setFrameShape(QFrame::StyledPanel);
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
auto* txRow = new QHBoxLayout(frame);
auto* hashLabel = new QLabel(tx.hash.left(16) + "...");
QFont boldFont = hashLabel->font();
boldFont.setBold(true);
hashLabel->setFont(boldFont);
QString typeColor;
switch (tx.type) {
case TransactionType::Public: typeColor = "#007bff"; break;
case TransactionType::PrivacyPreserving: typeColor = "#6f42c1"; break;
case TransactionType::ProgramDeployment: typeColor = "#fd7e14"; break;
}
auto* typeLabel = new QLabel(transactionTypeToString(tx.type));
typeLabel->setStyleSheet(Style::badge(typeColor));
auto* metaLabel = new QLabel();
switch (tx.type) {
case TransactionType::Public:
metaLabel->setText(QString("%1 accounts").arg(tx.accounts.size()));
break;
case TransactionType::PrivacyPreserving:
metaLabel->setText(QString("%1 accounts, %2 commitments").arg(tx.accounts.size()).arg(tx.newCommitmentsCount));
break;
case TransactionType::ProgramDeployment:
metaLabel->setText(QString("%1 bytes").arg(tx.bytecodeSizeBytes));
break;
}
metaLabel->setStyleSheet(Style::mutedText());
txRow->addWidget(hashLabel);
txRow->addWidget(typeLabel);
txRow->addWidget(metaLabel, 1);
QString txHash = tx.hash;
connect(frame, &ClickableFrame::clicked, this, [this, txHash]() {
emit transactionClicked(txHash);
});
layout->addWidget(frame);
}
}
scrollArea->setWidget(scrollContent);
outerLayout->addWidget(scrollArea);
}

16
src/pages/BlockPage.h Normal file
View File

@ -0,0 +1,16 @@
#pragma once
#include "models/Block.h"
#include <QWidget>
class BlockPage : public QWidget {
Q_OBJECT
public:
explicit BlockPage(const Block& block, QWidget* parent = nullptr);
signals:
void blockClicked(quint64 blockId);
void transactionClicked(const QString& hash);
};

256
src/pages/MainPage.cpp Normal file
View File

@ -0,0 +1,256 @@
#include "MainPage.h"
#include "Style.h"
#include "widgets/ClickableFrame.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QScrollArea>
#include <QFrame>
MainPage::MainPage(IndexerService* indexer, QWidget* parent)
: QWidget(parent)
, m_indexer(indexer)
{
auto* outerLayout = new QVBoxLayout(this);
// Health indicator
auto* healthRow = new QHBoxLayout();
m_healthLabel = new QLabel(this);
healthRow->addWidget(m_healthLabel);
healthRow->addStretch();
outerLayout->addLayout(healthRow);
// Scroll area for content
auto* scrollArea = new QScrollArea(this);
scrollArea->setWidgetResizable(true);
scrollArea->setFrameShape(QFrame::NoFrame);
auto* scrollContent = new QWidget();
m_contentLayout = new QVBoxLayout(scrollContent);
m_contentLayout->setAlignment(Qt::AlignTop);
scrollArea->setWidget(scrollContent);
outerLayout->addWidget(scrollArea);
refresh();
}
QWidget* MainPage::createSectionHeader(const QString& title)
{
auto* label = new QLabel(title);
QFont font = label->font();
font.setPointSize(16);
font.setBold(true);
label->setFont(font);
label->setStyleSheet("margin-top: 12px; margin-bottom: 4px;");
return label;
}
void MainPage::addBlockRow(QVBoxLayout* layout, const Block& block)
{
auto* frame = new ClickableFrame();
frame->setFrameShape(QFrame::StyledPanel);
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
auto* row = new QHBoxLayout(frame);
auto* idLabel = new QLabel(QString("Block #%1").arg(block.blockId));
QFont boldFont = idLabel->font();
boldFont.setBold(true);
idLabel->setFont(boldFont);
QString statusColor;
switch (block.bedrockStatus) {
case BedrockStatus::Finalized: statusColor = "#28a745"; break;
case BedrockStatus::Safe: statusColor = "#ffc107"; break;
case BedrockStatus::Pending: statusColor = "#6c757d"; break;
}
auto* statusLabel = new QLabel(bedrockStatusToString(block.bedrockStatus));
statusLabel->setStyleSheet(Style::badge(statusColor));
auto* hashLabel = new QLabel(block.hash.left(16) + "...");
hashLabel->setStyleSheet(Style::mutedText());
auto* txCount = new QLabel(QString("%1 tx").arg(block.transactions.size()));
auto* timeLabel = new QLabel(block.timestamp.toString("yyyy-MM-dd hh:mm:ss UTC"));
timeLabel->setStyleSheet(Style::mutedText());
row->addWidget(idLabel);
row->addWidget(statusLabel);
row->addWidget(hashLabel, 1);
row->addWidget(txCount);
row->addWidget(timeLabel);
quint64 blockId = block.blockId;
connect(frame, &ClickableFrame::clicked, this, [this, blockId]() {
emit blockClicked(blockId);
});
layout->addWidget(frame);
}
void MainPage::addTransactionRow(QVBoxLayout* layout, const Transaction& tx)
{
auto* frame = new ClickableFrame();
frame->setFrameShape(QFrame::StyledPanel);
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
auto* row = new QHBoxLayout(frame);
auto* hashLabel = new QLabel(tx.hash.left(16) + "...");
QFont boldFont = hashLabel->font();
boldFont.setBold(true);
hashLabel->setFont(boldFont);
QString typeColor;
switch (tx.type) {
case TransactionType::Public: typeColor = "#007bff"; break;
case TransactionType::PrivacyPreserving: typeColor = "#6f42c1"; break;
case TransactionType::ProgramDeployment: typeColor = "#fd7e14"; break;
}
auto* typeLabel = new QLabel(transactionTypeToString(tx.type));
typeLabel->setStyleSheet(Style::badge(typeColor));
auto* metaLabel = new QLabel();
switch (tx.type) {
case TransactionType::Public:
metaLabel->setText(QString("%1 accounts").arg(tx.accounts.size()));
break;
case TransactionType::PrivacyPreserving:
metaLabel->setText(QString("%1 accounts, %2 commitments").arg(tx.accounts.size()).arg(tx.newCommitmentsCount));
break;
case TransactionType::ProgramDeployment:
metaLabel->setText(QString("%1 bytes").arg(tx.bytecodeSizeBytes));
break;
}
metaLabel->setStyleSheet(Style::mutedText());
row->addWidget(hashLabel);
row->addWidget(typeLabel);
row->addWidget(metaLabel, 1);
QString txHash = tx.hash;
connect(frame, &ClickableFrame::clicked, this, [this, txHash]() {
emit transactionClicked(txHash);
});
layout->addWidget(frame);
}
void MainPage::addAccountRow(QVBoxLayout* layout, const Account& account)
{
auto* frame = new ClickableFrame();
frame->setFrameShape(QFrame::StyledPanel);
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
auto* row = new QHBoxLayout(frame);
auto* idLabel = new QLabel(account.accountId.left(16) + "...");
QFont boldFont = idLabel->font();
boldFont.setBold(true);
idLabel->setFont(boldFont);
auto* balanceLabel = new QLabel(QString("Balance: %1").arg(account.balance));
auto* nonceLabel = new QLabel(QString("Nonce: %1").arg(account.nonce));
nonceLabel->setStyleSheet(Style::mutedText());
row->addWidget(idLabel);
row->addWidget(balanceLabel, 1);
row->addWidget(nonceLabel);
QString accId = account.accountId;
connect(frame, &ClickableFrame::clicked, this, [this, accId]() {
emit accountClicked(accId);
});
layout->addWidget(frame);
}
void MainPage::refresh()
{
clearSearchResults();
quint64 latestId = m_indexer->getLatestBlockId();
m_healthLabel->setText(QString("Chain height: %1").arg(latestId));
m_healthLabel->setStyleSheet("color: #28a745; font-weight: bold; font-size: 14px;");
if (m_recentBlocksWidget) {
m_contentLayout->removeWidget(m_recentBlocksWidget);
delete m_recentBlocksWidget;
}
m_recentBlocksWidget = new QWidget();
auto* blocksLayout = new QVBoxLayout(m_recentBlocksWidget);
blocksLayout->setContentsMargins(0, 0, 0, 0);
blocksLayout->addWidget(createSectionHeader("Recent Blocks"));
auto blocks = m_indexer->getBlocks(std::nullopt, 10);
for (const auto& block : blocks) {
addBlockRow(blocksLayout, block);
}
m_contentLayout->addWidget(m_recentBlocksWidget);
}
void MainPage::showSearchResults(const SearchResults& results)
{
clearSearchResults();
if (m_recentBlocksWidget) {
m_recentBlocksWidget->hide();
}
m_searchResultsWidget = new QWidget();
auto* layout = new QVBoxLayout(m_searchResultsWidget);
layout->setContentsMargins(0, 0, 0, 0);
bool hasResults = false;
if (!results.blocks.isEmpty()) {
layout->addWidget(createSectionHeader("Blocks"));
for (const auto& block : results.blocks) {
addBlockRow(layout, block);
}
hasResults = true;
}
if (!results.transactions.isEmpty()) {
layout->addWidget(createSectionHeader("Transactions"));
for (const auto& tx : results.transactions) {
addTransactionRow(layout, tx);
}
hasResults = true;
}
if (!results.accounts.isEmpty()) {
layout->addWidget(createSectionHeader("Accounts"));
for (const auto& acc : results.accounts) {
addAccountRow(layout, acc);
}
hasResults = true;
}
if (!hasResults) {
auto* noResults = new QLabel("No results found.");
noResults->setAlignment(Qt::AlignCenter);
noResults->setStyleSheet(Style::mutedText() + " padding: 20px; font-size: 14px;");
layout->addWidget(noResults);
}
m_contentLayout->insertWidget(0, m_searchResultsWidget);
}
void MainPage::clearSearchResults()
{
if (m_searchResultsWidget) {
m_contentLayout->removeWidget(m_searchResultsWidget);
delete m_searchResultsWidget;
m_searchResultsWidget = nullptr;
}
if (m_recentBlocksWidget) {
m_recentBlocksWidget->show();
}
}

36
src/pages/MainPage.h Normal file
View File

@ -0,0 +1,36 @@
#pragma once
#include "services/IndexerService.h"
#include <QWidget>
class QVBoxLayout;
class QLabel;
class MainPage : public QWidget {
Q_OBJECT
public:
explicit MainPage(IndexerService* indexer, QWidget* parent = nullptr);
void refresh();
void showSearchResults(const SearchResults& results);
void clearSearchResults();
signals:
void blockClicked(quint64 blockId);
void transactionClicked(const QString& hash);
void accountClicked(const QString& accountId);
private:
void addBlockRow(QVBoxLayout* layout, const Block& block);
void addTransactionRow(QVBoxLayout* layout, const Transaction& tx);
void addAccountRow(QVBoxLayout* layout, const Account& account);
QWidget* createSectionHeader(const QString& title);
IndexerService* m_indexer = nullptr;
QVBoxLayout* m_contentLayout = nullptr;
QWidget* m_searchResultsWidget = nullptr;
QWidget* m_recentBlocksWidget = nullptr;
QLabel* m_healthLabel = nullptr;
};

View File

@ -0,0 +1,160 @@
#include "TransactionPage.h"
#include "Style.h"
#include "widgets/ClickableFrame.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QScrollArea>
#include <QFrame>
#include <QGridLayout>
namespace {
QLabel* makeFieldLabel(const QString& text)
{
auto* label = new QLabel(text);
label->setStyleSheet(Style::mutedText() + " font-weight: bold;");
return label;
}
QLabel* makeValueLabel(const QString& text)
{
auto* label = new QLabel(text);
label->setTextInteractionFlags(Qt::TextSelectableByMouse);
label->setWordWrap(true);
return label;
}
} // namespace
TransactionPage::TransactionPage(const Transaction& tx, QWidget* parent)
: QWidget(parent)
{
auto* outerLayout = new QVBoxLayout(this);
auto* scrollArea = new QScrollArea(this);
scrollArea->setWidgetResizable(true);
scrollArea->setFrameShape(QFrame::NoFrame);
auto* scrollContent = new QWidget();
auto* layout = new QVBoxLayout(scrollContent);
layout->setAlignment(Qt::AlignTop);
// Title
auto* title = new QLabel("Transaction Details");
QFont titleFont = title->font();
titleFont.setPointSize(20);
titleFont.setBold(true);
title->setFont(titleFont);
layout->addWidget(title);
// Transaction info grid
auto* infoFrame = new QFrame();
infoFrame->setFrameShape(QFrame::StyledPanel);
infoFrame->setStyleSheet(Style::cardFrameWithLabels());
auto* grid = new QGridLayout(infoFrame);
grid->setColumnStretch(1, 1);
int row = 0;
grid->addWidget(makeFieldLabel("Hash"), row, 0);
auto* hashVal = makeValueLabel(tx.hash);
hashVal->setStyleSheet(Style::monoText());
grid->addWidget(hashVal, row++, 1);
grid->addWidget(makeFieldLabel("Type"), row, 0);
QString typeColor;
switch (tx.type) {
case TransactionType::Public: typeColor = "#007bff"; break;
case TransactionType::PrivacyPreserving: typeColor = "#6f42c1"; break;
case TransactionType::ProgramDeployment: typeColor = "#fd7e14"; break;
}
auto* typeLabel = new QLabel(transactionTypeToString(tx.type));
typeLabel->setStyleSheet(Style::badge(typeColor) + " max-width: 160px;");
grid->addWidget(typeLabel, row++, 1);
// Type-specific fields
switch (tx.type) {
case TransactionType::Public:
grid->addWidget(makeFieldLabel("Program ID"), row, 0);
grid->addWidget(makeValueLabel(tx.programId), row++, 1);
grid->addWidget(makeFieldLabel("Instruction Data"), row, 0);
grid->addWidget(makeValueLabel(QString("%1 items").arg(tx.instructionData.size())), row++, 1);
grid->addWidget(makeFieldLabel("Proof Size"), row, 0);
grid->addWidget(makeValueLabel(QString("%1 bytes").arg(tx.proofSizeBytes)), row++, 1);
grid->addWidget(makeFieldLabel("Signatures"), row, 0);
grid->addWidget(makeValueLabel(QString::number(tx.signatureCount)), row++, 1);
break;
case TransactionType::PrivacyPreserving:
grid->addWidget(makeFieldLabel("Public Accounts"), row, 0);
grid->addWidget(makeValueLabel(QString::number(tx.accounts.size())), row++, 1);
grid->addWidget(makeFieldLabel("New Commitments"), row, 0);
grid->addWidget(makeValueLabel(QString::number(tx.newCommitmentsCount)), row++, 1);
grid->addWidget(makeFieldLabel("Nullifiers"), row, 0);
grid->addWidget(makeValueLabel(QString::number(tx.nullifiersCount)), row++, 1);
grid->addWidget(makeFieldLabel("Encrypted States"), row, 0);
grid->addWidget(makeValueLabel(QString::number(tx.encryptedStatesCount)), row++, 1);
grid->addWidget(makeFieldLabel("Proof Size"), row, 0);
grid->addWidget(makeValueLabel(QString("%1 bytes").arg(tx.proofSizeBytes)), row++, 1);
grid->addWidget(makeFieldLabel("Validity Window"), row, 0);
grid->addWidget(makeValueLabel(QString("[%1, %2)").arg(tx.validityWindowStart).arg(tx.validityWindowEnd)), row++, 1);
break;
case TransactionType::ProgramDeployment:
grid->addWidget(makeFieldLabel("Bytecode Size"), row, 0);
grid->addWidget(makeValueLabel(QString("%1 bytes").arg(tx.bytecodeSizeBytes)), row++, 1);
break;
}
layout->addWidget(infoFrame);
// Accounts section
if (!tx.accounts.isEmpty()) {
auto* accHeader = new QLabel("Accounts");
QFont headerFont = accHeader->font();
headerFont.setPointSize(16);
headerFont.setBold(true);
accHeader->setFont(headerFont);
accHeader->setStyleSheet("margin-top: 16px; margin-bottom: 4px;");
layout->addWidget(accHeader);
for (const auto& accRef : tx.accounts) {
auto* frame = new ClickableFrame();
frame->setFrameShape(QFrame::StyledPanel);
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
auto* accRow = new QHBoxLayout(frame);
auto* idLabel = new QLabel(accRef.accountId.left(20) + "...");
QFont boldFont = idLabel->font();
boldFont.setBold(true);
idLabel->setFont(boldFont);
auto* nonceLabel = new QLabel(QString("Nonce: %1").arg(accRef.nonce));
nonceLabel->setStyleSheet(Style::mutedText());
accRow->addWidget(idLabel, 1);
accRow->addWidget(nonceLabel);
QString accId = accRef.accountId;
connect(frame, &ClickableFrame::clicked, this, [this, accId]() {
emit accountClicked(accId);
});
layout->addWidget(frame);
}
}
scrollArea->setWidget(scrollContent);
outerLayout->addWidget(scrollArea);
}

View File

@ -0,0 +1,15 @@
#pragma once
#include "models/Transaction.h"
#include <QWidget>
class TransactionPage : public QWidget {
Q_OBJECT
public:
explicit TransactionPage(const Transaction& tx, QWidget* parent = nullptr);
signals:
void accountClicked(const QString& accountId);
};

View File

@ -0,0 +1,28 @@
#pragma once
#include "models/Block.h"
#include "models/Transaction.h"
#include "models/Account.h"
#include <QVector>
#include <optional>
struct SearchResults {
QVector<Block> blocks;
QVector<Transaction> transactions;
QVector<Account> accounts;
};
class IndexerService {
public:
virtual ~IndexerService() = default;
virtual std::optional<Account> getAccount(const QString& accountId) = 0;
virtual std::optional<Block> getBlockById(quint64 blockId) = 0;
virtual std::optional<Block> getBlockByHash(const QString& hash) = 0;
virtual std::optional<Transaction> getTransaction(const QString& hash) = 0;
virtual QVector<Block> getBlocks(std::optional<quint64> before, int limit) = 0;
virtual quint64 getLatestBlockId() = 0;
virtual QVector<Transaction> getTransactionsByAccount(const QString& accountId, int offset, int limit) = 0;
virtual SearchResults search(const QString& query) = 0;
};

View File

@ -0,0 +1,308 @@
#include "MockIndexerService.h"
#include <QRandomGenerator>
#include <algorithm>
namespace {
QString randomHexString(int bytes)
{
QString result;
result.reserve(bytes * 2);
auto* rng = QRandomGenerator::global();
for (int i = 0; i < bytes; ++i) {
result += QString("%1").arg(rng->bounded(256), 2, 16, QChar('0'));
}
return result;
}
// Generate a base58-like string (simplified — uses alphanumeric chars without 0/O/I/l)
QString randomBase58String(int length)
{
static const char chars[] = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
QString result;
result.reserve(length);
auto* rng = QRandomGenerator::global();
for (int i = 0; i < length; ++i) {
result += chars[rng->bounded(static_cast<int>(sizeof(chars) - 1))];
}
return result;
}
} // namespace
MockIndexerService::MockIndexerService()
{
generateData();
}
QString MockIndexerService::randomHash()
{
return randomHexString(32);
}
QString MockIndexerService::randomAccountId()
{
return randomBase58String(44);
}
Transaction MockIndexerService::generatePublicTransaction()
{
auto* rng = QRandomGenerator::global();
Transaction tx;
tx.hash = randomHash();
tx.type = TransactionType::Public;
tx.programId = randomBase58String(44);
tx.signatureCount = rng->bounded(1, 4);
tx.proofSizeBytes = rng->bounded(0, 2048);
int accountCount = rng->bounded(1, 5);
for (int i = 0; i < accountCount; ++i) {
// Reuse existing accounts sometimes
QString accId;
if (!m_accounts.isEmpty() && rng->bounded(100) < 60) {
auto keys = m_accounts.keys();
accId = keys[rng->bounded(keys.size())];
} else {
accId = randomAccountId();
}
tx.accounts.append({accId, QString::number(rng->bounded(1, 100))});
}
int instrCount = rng->bounded(1, 8);
for (int i = 0; i < instrCount; ++i) {
tx.instructionData.append(rng->generate());
}
return tx;
}
Transaction MockIndexerService::generatePrivacyPreservingTransaction()
{
auto* rng = QRandomGenerator::global();
Transaction tx;
tx.hash = randomHash();
tx.type = TransactionType::PrivacyPreserving;
tx.proofSizeBytes = rng->bounded(512, 4096);
tx.newCommitmentsCount = rng->bounded(1, 6);
tx.nullifiersCount = rng->bounded(1, 4);
tx.encryptedStatesCount = rng->bounded(1, 3);
tx.validityWindowStart = rng->bounded(1, 50);
tx.validityWindowEnd = tx.validityWindowStart + rng->bounded(50, 200);
int accountCount = rng->bounded(1, 4);
for (int i = 0; i < accountCount; ++i) {
QString accId;
if (!m_accounts.isEmpty() && rng->bounded(100) < 60) {
auto keys = m_accounts.keys();
accId = keys[rng->bounded(keys.size())];
} else {
accId = randomAccountId();
}
tx.accounts.append({accId, QString::number(rng->bounded(1, 100))});
}
return tx;
}
Transaction MockIndexerService::generateProgramDeploymentTransaction()
{
auto* rng = QRandomGenerator::global();
Transaction tx;
tx.hash = randomHash();
tx.type = TransactionType::ProgramDeployment;
tx.bytecodeSizeBytes = rng->bounded(1024, 65536);
tx.signatureCount = 1;
return tx;
}
void MockIndexerService::generateData()
{
auto* rng = QRandomGenerator::global();
// Generate accounts
for (int i = 0; i < 15; ++i) {
Account acc;
acc.accountId = randomAccountId();
acc.programOwner = randomBase58String(44);
acc.balance = QString::number(rng->bounded(0, 1000000));
acc.nonce = QString::number(rng->bounded(0, 500));
acc.dataSizeBytes = rng->bounded(0, 4096);
m_accounts[acc.accountId] = acc;
}
// Generate blocks
QString prevHash = QString(64, '0');
QDateTime timestamp = QDateTime::currentDateTimeUtc().addSecs(-25 * 12); // ~5 min ago
for (quint64 id = 1; id <= 25; ++id) {
Block block;
block.blockId = id;
block.prevBlockHash = prevHash;
block.hash = randomHash();
block.timestamp = timestamp;
block.signature = randomHexString(64);
block.bedrockParentId = randomHexString(32);
// Older blocks are finalized, middle ones safe, recent ones pending
if (id <= 15) {
block.bedrockStatus = BedrockStatus::Finalized;
} else if (id <= 22) {
block.bedrockStatus = BedrockStatus::Safe;
} else {
block.bedrockStatus = BedrockStatus::Pending;
}
// Generate 1-5 transactions per block
int txCount = rng->bounded(1, 6);
for (int t = 0; t < txCount; ++t) {
Transaction tx;
int typeRoll = rng->bounded(100);
if (typeRoll < 60) {
tx = generatePublicTransaction();
} else if (typeRoll < 85) {
tx = generatePrivacyPreservingTransaction();
} else {
tx = generateProgramDeploymentTransaction();
}
block.transactions.append(tx);
m_transactionsByHash[tx.hash] = tx;
// Ensure accounts referenced in transactions exist
for (const auto& accRef : tx.accounts) {
if (!m_accounts.contains(accRef.accountId)) {
Account acc;
acc.accountId = accRef.accountId;
acc.programOwner = randomBase58String(44);
acc.balance = QString::number(rng->bounded(0, 1000000));
acc.nonce = accRef.nonce;
acc.dataSizeBytes = rng->bounded(0, 4096);
m_accounts[acc.accountId] = acc;
}
}
}
m_blocks.append(block);
m_blocksByHash[block.hash] = block;
prevHash = block.hash;
timestamp = timestamp.addSecs(12);
}
}
std::optional<Account> MockIndexerService::getAccount(const QString& accountId)
{
auto it = m_accounts.find(accountId);
if (it != m_accounts.end()) {
return *it;
}
return std::nullopt;
}
std::optional<Block> MockIndexerService::getBlockById(quint64 blockId)
{
if (blockId >= 1 && blockId <= static_cast<quint64>(m_blocks.size())) {
return m_blocks[static_cast<int>(blockId - 1)];
}
return std::nullopt;
}
std::optional<Block> MockIndexerService::getBlockByHash(const QString& hash)
{
auto it = m_blocksByHash.find(hash);
if (it != m_blocksByHash.end()) {
return *it;
}
return std::nullopt;
}
std::optional<Transaction> MockIndexerService::getTransaction(const QString& hash)
{
auto it = m_transactionsByHash.find(hash);
if (it != m_transactionsByHash.end()) {
return *it;
}
return std::nullopt;
}
QVector<Block> MockIndexerService::getBlocks(std::optional<quint64> before, int limit)
{
QVector<Block> result;
int startIdx = m_blocks.size() - 1;
if (before.has_value()) {
startIdx = static_cast<int>(*before) - 2; // -1 for 0-index, -1 for "before"
}
for (int i = startIdx; i >= 0 && result.size() < limit; --i) {
result.append(m_blocks[i]);
}
return result;
}
quint64 MockIndexerService::getLatestBlockId()
{
if (m_blocks.isEmpty()) return 0;
return m_blocks.last().blockId;
}
QVector<Transaction> MockIndexerService::getTransactionsByAccount(const QString& accountId, int offset, int limit)
{
QVector<Transaction> result;
// Search all transactions for ones referencing this account
for (auto it = m_transactionsByHash.begin(); it != m_transactionsByHash.end(); ++it) {
for (const auto& accRef : it->accounts) {
if (accRef.accountId == accountId) {
result.append(*it);
break;
}
}
}
// Apply offset and limit
if (offset >= result.size()) return {};
return result.mid(offset, limit);
}
SearchResults MockIndexerService::search(const QString& query)
{
SearchResults results;
QString trimmed = query.trimmed();
if (trimmed.isEmpty()) return results;
// Try as block ID (numeric)
bool ok = false;
quint64 blockId = trimmed.toULongLong(&ok);
if (ok) {
auto block = getBlockById(blockId);
if (block) {
results.blocks.append(*block);
}
}
// Try as hash (hex string, 64 chars)
if (trimmed.size() == 64) {
auto block = getBlockByHash(trimmed);
if (block) {
results.blocks.append(*block);
}
auto tx = getTransaction(trimmed);
if (tx) {
results.transactions.append(*tx);
}
}
// Try as account ID
auto account = getAccount(trimmed);
if (account) {
results.accounts.append(*account);
}
return results;
}

View File

@ -0,0 +1,32 @@
#pragma once
#include "IndexerService.h"
#include <QMap>
class MockIndexerService : public IndexerService {
public:
MockIndexerService();
std::optional<Account> getAccount(const QString& accountId) override;
std::optional<Block> getBlockById(quint64 blockId) override;
std::optional<Block> getBlockByHash(const QString& hash) override;
std::optional<Transaction> getTransaction(const QString& hash) override;
QVector<Block> getBlocks(std::optional<quint64> before, int limit) override;
quint64 getLatestBlockId() override;
QVector<Transaction> getTransactionsByAccount(const QString& accountId, int offset, int limit) override;
SearchResults search(const QString& query) override;
private:
void generateData();
QString randomHash();
QString randomAccountId();
Transaction generatePublicTransaction();
Transaction generatePrivacyPreservingTransaction();
Transaction generateProgramDeploymentTransaction();
QVector<Block> m_blocks;
QMap<QString, Block> m_blocksByHash;
QMap<QString, Transaction> m_transactionsByHash;
QMap<QString, Account> m_accounts;
};

View File

@ -0,0 +1,27 @@
#pragma once
#include <QFrame>
#include <QMouseEvent>
class ClickableFrame : public QFrame {
Q_OBJECT
public:
explicit ClickableFrame(QWidget* parent = nullptr)
: QFrame(parent)
{
setCursor(Qt::PointingHandCursor);
}
signals:
void clicked();
protected:
void mousePressEvent(QMouseEvent* event) override
{
if (event->button() == Qt::LeftButton) {
emit clicked();
}
QFrame::mousePressEvent(event);
}
};

View File

@ -0,0 +1,45 @@
#include "NavigationBar.h"
#include <QHBoxLayout>
#include <QPushButton>
NavigationBar::NavigationBar(QWidget* parent)
: QWidget(parent)
{
auto* layout = new QHBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
m_backBtn = new QPushButton("<", this);
m_backBtn->setFixedWidth(40);
m_backBtn->setMinimumHeight(32);
m_backBtn->setEnabled(false);
m_backBtn->setToolTip("Back");
m_forwardBtn = new QPushButton(">", this);
m_forwardBtn->setFixedWidth(40);
m_forwardBtn->setMinimumHeight(32);
m_forwardBtn->setEnabled(false);
m_forwardBtn->setToolTip("Forward");
auto* homeBtn = new QPushButton("Home", this);
homeBtn->setMinimumHeight(32);
layout->addWidget(m_backBtn);
layout->addWidget(m_forwardBtn);
layout->addWidget(homeBtn);
layout->addStretch();
connect(m_backBtn, &QPushButton::clicked, this, &NavigationBar::backClicked);
connect(m_forwardBtn, &QPushButton::clicked, this, &NavigationBar::forwardClicked);
connect(homeBtn, &QPushButton::clicked, this, &NavigationBar::homeClicked);
}
void NavigationBar::setBackEnabled(bool enabled)
{
m_backBtn->setEnabled(enabled);
}
void NavigationBar::setForwardEnabled(bool enabled)
{
m_forwardBtn->setEnabled(enabled);
}

View File

@ -0,0 +1,24 @@
#pragma once
#include <QWidget>
class QPushButton;
class NavigationBar : public QWidget {
Q_OBJECT
public:
explicit NavigationBar(QWidget* parent = nullptr);
void setBackEnabled(bool enabled);
void setForwardEnabled(bool enabled);
signals:
void backClicked();
void forwardClicked();
void homeClicked();
private:
QPushButton* m_backBtn = nullptr;
QPushButton* m_forwardBtn = nullptr;
};

34
src/widgets/SearchBar.cpp Normal file
View File

@ -0,0 +1,34 @@
#include "SearchBar.h"
#include <QHBoxLayout>
#include <QLineEdit>
#include <QPushButton>
SearchBar::SearchBar(QWidget* parent)
: QWidget(parent)
{
auto* layout = new QHBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
m_input = new QLineEdit(this);
m_input->setPlaceholderText("Search by block ID / block hash / tx hash / account ID...");
m_input->setMinimumHeight(32);
auto* searchBtn = new QPushButton("Search", this);
searchBtn->setMinimumHeight(32);
layout->addWidget(m_input, 1);
layout->addWidget(searchBtn);
connect(m_input, &QLineEdit::returnPressed, this, [this]() {
emit searchRequested(m_input->text());
});
connect(searchBtn, &QPushButton::clicked, this, [this]() {
emit searchRequested(m_input->text());
});
}
void SearchBar::clear()
{
m_input->clear();
}

20
src/widgets/SearchBar.h Normal file
View File

@ -0,0 +1,20 @@
#pragma once
#include <QWidget>
class QLineEdit;
class SearchBar : public QWidget {
Q_OBJECT
public:
explicit SearchBar(QWidget* parent = nullptr);
void clear();
signals:
void searchRequested(const QString& query);
private:
QLineEdit* m_input = nullptr;
};