diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b2be92b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +result diff --git a/CMakeLists.txt b/CMakeLists.txt index 1cb8436..e6ae9cc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/src/ExplorerWidget.cpp b/src/ExplorerWidget.cpp index 76b72bb..7149ff9 100644 --- a/src/ExplorerWidget.cpp +++ b/src/ExplorerWidget.cpp @@ -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 #include +#include #include ExplorerWidget::ExplorerWidget(QWidget* parent) : QWidget(parent) + , m_indexer(std::make_unique()) { 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(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(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(page)) { + connect(txPage, &TransactionPage::accountClicked, this, [this](const QString& id) { + navigateTo(NavAccount{id}); + }); + } else if (auto* accPage = qobject_cast(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; + + if constexpr (std::is_same_v) { + m_mainPage->clearSearchResults(); + m_mainPage->refresh(); + showPage(m_mainPage); + } else if constexpr (std::is_same_v) { + 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) { + 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) { + 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()); } diff --git a/src/ExplorerWidget.h b/src/ExplorerWidget.h index 341247f..b91f885 100644 --- a/src/ExplorerWidget.h +++ b/src/ExplorerWidget.h @@ -1,13 +1,49 @@ #pragma once +#include "services/IndexerService.h" + #include +#include +#include +#include + +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; 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 m_indexer; + NavigationBar* m_navBar = nullptr; + SearchBar* m_searchBar = nullptr; + QStackedWidget* m_stack = nullptr; + MainPage* m_mainPage = nullptr; + + QStack m_backHistory; + QStack m_forwardHistory; + NavTarget m_currentTarget; }; diff --git a/src/Style.h b/src/Style.h new file mode 100644 index 0000000..108c11e --- /dev/null +++ b/src/Style.h @@ -0,0 +1,44 @@ +#pragma once + +#include + +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 diff --git a/src/models/Account.h b/src/models/Account.h new file mode 100644 index 0000000..d7eaaa1 --- /dev/null +++ b/src/models/Account.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +struct Account { + QString accountId; + QString programOwner; + QString balance; + QString nonce; + int dataSizeBytes = 0; +}; diff --git a/src/models/Block.h b/src/models/Block.h new file mode 100644 index 0000000..6c9acce --- /dev/null +++ b/src/models/Block.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include + +#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 transactions; + BedrockStatus bedrockStatus = BedrockStatus::Pending; + QString bedrockParentId; +}; diff --git a/src/models/Transaction.h b/src/models/Transaction.h new file mode 100644 index 0000000..09a9246 --- /dev/null +++ b/src/models/Transaction.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include + +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 accounts; + QVector 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; +}; diff --git a/src/pages/AccountPage.cpp b/src/pages/AccountPage.cpp new file mode 100644 index 0000000..af37159 --- /dev/null +++ b/src/pages/AccountPage.cpp @@ -0,0 +1,134 @@ +#include "AccountPage.h" +#include "Style.h" +#include "widgets/ClickableFrame.h" + +#include +#include +#include +#include +#include +#include + +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); +} diff --git a/src/pages/AccountPage.h b/src/pages/AccountPage.h new file mode 100644 index 0000000..083216f --- /dev/null +++ b/src/pages/AccountPage.h @@ -0,0 +1,16 @@ +#pragma once + +#include "models/Account.h" +#include "services/IndexerService.h" + +#include + +class AccountPage : public QWidget { + Q_OBJECT + +public: + explicit AccountPage(const Account& account, IndexerService* indexer, QWidget* parent = nullptr); + +signals: + void transactionClicked(const QString& hash); +}; diff --git a/src/pages/BlockPage.cpp b/src/pages/BlockPage.cpp new file mode 100644 index 0000000..43cc449 --- /dev/null +++ b/src/pages/BlockPage.cpp @@ -0,0 +1,162 @@ +#include "BlockPage.h" +#include "Style.h" +#include "widgets/ClickableFrame.h" + +#include +#include +#include +#include +#include +#include + +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("%1").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); +} diff --git a/src/pages/BlockPage.h b/src/pages/BlockPage.h new file mode 100644 index 0000000..d870e0f --- /dev/null +++ b/src/pages/BlockPage.h @@ -0,0 +1,16 @@ +#pragma once + +#include "models/Block.h" + +#include + +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); +}; diff --git a/src/pages/MainPage.cpp b/src/pages/MainPage.cpp new file mode 100644 index 0000000..f9e8c37 --- /dev/null +++ b/src/pages/MainPage.cpp @@ -0,0 +1,256 @@ +#include "MainPage.h" +#include "Style.h" +#include "widgets/ClickableFrame.h" + +#include +#include +#include +#include +#include + +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(); + } +} diff --git a/src/pages/MainPage.h b/src/pages/MainPage.h new file mode 100644 index 0000000..7086d6f --- /dev/null +++ b/src/pages/MainPage.h @@ -0,0 +1,36 @@ +#pragma once + +#include "services/IndexerService.h" + +#include + +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; +}; diff --git a/src/pages/TransactionPage.cpp b/src/pages/TransactionPage.cpp new file mode 100644 index 0000000..58b3109 --- /dev/null +++ b/src/pages/TransactionPage.cpp @@ -0,0 +1,160 @@ +#include "TransactionPage.h" +#include "Style.h" +#include "widgets/ClickableFrame.h" + +#include +#include +#include +#include +#include +#include + +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); +} diff --git a/src/pages/TransactionPage.h b/src/pages/TransactionPage.h new file mode 100644 index 0000000..7f6d978 --- /dev/null +++ b/src/pages/TransactionPage.h @@ -0,0 +1,15 @@ +#pragma once + +#include "models/Transaction.h" + +#include + +class TransactionPage : public QWidget { + Q_OBJECT + +public: + explicit TransactionPage(const Transaction& tx, QWidget* parent = nullptr); + +signals: + void accountClicked(const QString& accountId); +}; diff --git a/src/services/IndexerService.h b/src/services/IndexerService.h new file mode 100644 index 0000000..ac27ba4 --- /dev/null +++ b/src/services/IndexerService.h @@ -0,0 +1,28 @@ +#pragma once + +#include "models/Block.h" +#include "models/Transaction.h" +#include "models/Account.h" + +#include +#include + +struct SearchResults { + QVector blocks; + QVector transactions; + QVector accounts; +}; + +class IndexerService { +public: + virtual ~IndexerService() = default; + + virtual std::optional getAccount(const QString& accountId) = 0; + virtual std::optional getBlockById(quint64 blockId) = 0; + virtual std::optional getBlockByHash(const QString& hash) = 0; + virtual std::optional getTransaction(const QString& hash) = 0; + virtual QVector getBlocks(std::optional before, int limit) = 0; + virtual quint64 getLatestBlockId() = 0; + virtual QVector getTransactionsByAccount(const QString& accountId, int offset, int limit) = 0; + virtual SearchResults search(const QString& query) = 0; +}; diff --git a/src/services/MockIndexerService.cpp b/src/services/MockIndexerService.cpp new file mode 100644 index 0000000..f4262cb --- /dev/null +++ b/src/services/MockIndexerService.cpp @@ -0,0 +1,308 @@ +#include "MockIndexerService.h" + +#include +#include + +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(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 MockIndexerService::getAccount(const QString& accountId) +{ + auto it = m_accounts.find(accountId); + if (it != m_accounts.end()) { + return *it; + } + return std::nullopt; +} + +std::optional MockIndexerService::getBlockById(quint64 blockId) +{ + if (blockId >= 1 && blockId <= static_cast(m_blocks.size())) { + return m_blocks[static_cast(blockId - 1)]; + } + return std::nullopt; +} + +std::optional MockIndexerService::getBlockByHash(const QString& hash) +{ + auto it = m_blocksByHash.find(hash); + if (it != m_blocksByHash.end()) { + return *it; + } + return std::nullopt; +} + +std::optional MockIndexerService::getTransaction(const QString& hash) +{ + auto it = m_transactionsByHash.find(hash); + if (it != m_transactionsByHash.end()) { + return *it; + } + return std::nullopt; +} + +QVector MockIndexerService::getBlocks(std::optional before, int limit) +{ + QVector result; + int startIdx = m_blocks.size() - 1; + + if (before.has_value()) { + startIdx = static_cast(*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 MockIndexerService::getTransactionsByAccount(const QString& accountId, int offset, int limit) +{ + QVector 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; +} diff --git a/src/services/MockIndexerService.h b/src/services/MockIndexerService.h new file mode 100644 index 0000000..9b6b1b2 --- /dev/null +++ b/src/services/MockIndexerService.h @@ -0,0 +1,32 @@ +#pragma once + +#include "IndexerService.h" + +#include + +class MockIndexerService : public IndexerService { +public: + MockIndexerService(); + + std::optional getAccount(const QString& accountId) override; + std::optional getBlockById(quint64 blockId) override; + std::optional getBlockByHash(const QString& hash) override; + std::optional getTransaction(const QString& hash) override; + QVector getBlocks(std::optional before, int limit) override; + quint64 getLatestBlockId() override; + QVector 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 m_blocks; + QMap m_blocksByHash; + QMap m_transactionsByHash; + QMap m_accounts; +}; diff --git a/src/widgets/ClickableFrame.h b/src/widgets/ClickableFrame.h new file mode 100644 index 0000000..38b4661 --- /dev/null +++ b/src/widgets/ClickableFrame.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +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); + } +}; diff --git a/src/widgets/NavigationBar.cpp b/src/widgets/NavigationBar.cpp new file mode 100644 index 0000000..44c62ab --- /dev/null +++ b/src/widgets/NavigationBar.cpp @@ -0,0 +1,45 @@ +#include "NavigationBar.h" + +#include +#include + +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); +} diff --git a/src/widgets/NavigationBar.h b/src/widgets/NavigationBar.h new file mode 100644 index 0000000..836efe3 --- /dev/null +++ b/src/widgets/NavigationBar.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +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; +}; diff --git a/src/widgets/SearchBar.cpp b/src/widgets/SearchBar.cpp new file mode 100644 index 0000000..1aff9ab --- /dev/null +++ b/src/widgets/SearchBar.cpp @@ -0,0 +1,34 @@ +#include "SearchBar.h" + +#include +#include +#include + +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(); +} diff --git a/src/widgets/SearchBar.h b/src/widgets/SearchBar.h new file mode 100644 index 0000000..09a7235 --- /dev/null +++ b/src/widgets/SearchBar.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +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; +};