Merge 35ccd60be1eec8e2e4eeea2945f6284a9b2bd5b8 into 65a3a59cf7aee43202f32714a7b2f712e7e77e80

This commit is contained in:
Daniil Polyakov 2026-04-02 23:14:31 +03:00 committed by GitHub
commit ebcb409721
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 2745 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
result

112
CMakeLists.txt Normal file
View File

@ -0,0 +1,112 @@
cmake_minimum_required(VERSION 3.16)
project(LEZExplorerUIPlugin VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
# Find Qt packages
find_package(Qt6 REQUIRED COMPONENTS Core Widgets Svg SvgWidgets)
# Try to find the component-interfaces package first
find_package(component-interfaces QUIET)
# If not found, use the local interfaces folder
if(NOT component-interfaces_FOUND)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/interfaces)
add_library(component-interfaces INTERFACE)
target_include_directories(component-interfaces INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/interfaces)
endif()
# Source files
set(SOURCES
src/ExplorerPlugin.cpp
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
src/explorer_resources.qrc
)
# Create the plugin library
add_library(lez_explorer_ui SHARED ${SOURCES})
# Set output name without lib prefix
set_target_properties(lez_explorer_ui PROPERTIES
PREFIX ""
OUTPUT_NAME "lez_explorer_ui"
)
# Include directories
target_include_directories(lez_explorer_ui PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_BINARY_DIR}
)
# Link against libraries
target_link_libraries(lez_explorer_ui PRIVATE
Qt6::Core
Qt6::Widgets
Qt6::Svg
Qt6::SvgWidgets
component-interfaces
)
# Set common properties for both platforms
set_target_properties(lez_explorer_ui PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/modules"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/modules"
BUILD_WITH_INSTALL_RPATH TRUE
SKIP_BUILD_RPATH FALSE
)
if(APPLE)
set_target_properties(lez_explorer_ui PROPERTIES
INSTALL_RPATH "@loader_path"
INSTALL_NAME_DIR "@rpath"
BUILD_WITH_INSTALL_NAME_DIR TRUE
)
add_custom_command(TARGET lez_explorer_ui POST_BUILD
COMMAND install_name_tool -id "@rpath/lez_explorer_ui.dylib" $<TARGET_FILE:lez_explorer_ui>
COMMENT "Updating library paths for macOS"
)
else()
set_target_properties(lez_explorer_ui PROPERTIES
INSTALL_RPATH "$ORIGIN"
INSTALL_RPATH_USE_LINK_PATH FALSE
)
endif()
install(TARGETS lez_explorer_ui
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/logos/modules
RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR}/logos/modules
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}/logos/modules
)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/metadata.json
DESTINATION ${CMAKE_INSTALL_DATADIR}/lez-explorer-ui
)
message(STATUS "LEZ Explorer UI Plugin configured successfully")

45
README.md Normal file
View File

@ -0,0 +1,45 @@
# LEZ Explorer UI
A Qt6/C++ block explorer for the **Logos Execution Zone** (LEZ). Builds as both a shared library (Qt plugin) and a standalone desktop application.
## Features
- Browse recent blocks with live streaming of new blocks
- View block details: hash, previous hash, timestamp, status, signature, transactions
- Navigate to previous blocks via clickable hash links
- View transaction details (Public, Privacy-Preserving, Program Deployment)
- View account details with paginated transaction history
- Full-text search by block ID, block hash, transaction hash, or account ID
- Browser-style back/forward navigation
- Pagination with "Load more" for older blocks
- Dark theme (Catppuccin Mocha)
## Building with Nix
### Standalone application
```sh
nix build '.#app'
nix run '.#app'
```
### Shared library (Qt plugin)
```sh
nix build '.#lib'
```
### Development shell
```sh
nix develop
```
## License
Licensed under either of
- Apache License, Version 2.0 ([LICENSE-APACHE-v2](LICENSE-APACHE-v2))
- MIT License ([LICENSE-MIT](LICENSE-MIT))
at your option.

44
app/CMakeLists.txt Normal file
View File

@ -0,0 +1,44 @@
cmake_minimum_required(VERSION 3.16)
project(LEZExplorerUIApp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
# Find Qt packages
find_package(Qt6 REQUIRED COMPONENTS Core Widgets)
# Set output directories
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
# Create the executable
add_executable(lez-explorer-ui-app
main.cpp
mainwindow.cpp
mainwindow.h
)
# Link libraries
target_link_libraries(lez-explorer-ui-app PRIVATE
Qt6::Core
Qt6::Widgets
)
# Set RPATH settings for the executable
if(APPLE)
set_target_properties(lez-explorer-ui-app PROPERTIES
INSTALL_RPATH "@executable_path/../lib"
BUILD_WITH_INSTALL_RPATH TRUE
)
elseif(UNIX)
set_target_properties(lez-explorer-ui-app PROPERTIES
INSTALL_RPATH "$ORIGIN/../lib"
BUILD_WITH_INSTALL_RPATH TRUE
)
endif()
# Install rules
install(TARGETS lez-explorer-ui-app
RUNTIME DESTINATION bin
)

13
app/main.cpp Normal file
View File

@ -0,0 +1,13 @@
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
MainWindow window;
window.show();
return app.exec();
}

60
app/mainwindow.cpp Normal file
View File

@ -0,0 +1,60 @@
#include "mainwindow.h"
#include <QtWidgets>
MainWindow::MainWindow(QWidget* parent)
: QMainWindow(parent)
{
setupUi();
}
MainWindow::~MainWindow() = default;
void MainWindow::setupUi()
{
// Determine the appropriate plugin extension based on the platform
QString pluginExtension;
#if defined(Q_OS_WIN)
pluginExtension = ".dll";
#elif defined(Q_OS_MAC)
pluginExtension = ".dylib";
#else
pluginExtension = ".so";
#endif
QString pluginPath = QCoreApplication::applicationDirPath() + "/../lez_explorer_ui" + pluginExtension;
QPluginLoader loader(pluginPath);
QWidget* explorerWidget = nullptr;
if (loader.load()) {
QObject* plugin = loader.instance();
if (plugin) {
QMetaObject::invokeMethod(plugin, "createWidget",
Qt::DirectConnection,
Q_RETURN_ARG(QWidget*, explorerWidget));
}
}
if (explorerWidget) {
setCentralWidget(explorerWidget);
} else {
qWarning() << "Failed to load LEZ Explorer UI plugin from:" << pluginPath;
qWarning() << "Error:" << loader.errorString();
auto* fallbackWidget = new QWidget(this);
auto* layout = new QVBoxLayout(fallbackWidget);
auto* messageLabel = new QLabel("LEZ Explorer UI module not loaded", fallbackWidget);
QFont font = messageLabel->font();
font.setPointSize(14);
messageLabel->setFont(font);
messageLabel->setAlignment(Qt::AlignCenter);
layout->addWidget(messageLabel);
setCentralWidget(fallbackWidget);
}
setWindowTitle("LEZ Explorer");
resize(1024, 768);
}

14
app/mainwindow.h Normal file
View File

@ -0,0 +1,14 @@
#pragma once
#include <QMainWindow>
class MainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(QWidget* parent = nullptr);
~MainWindow() override;
private:
void setupUi();
};

27
flake.lock generated Normal file
View File

@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1751274312,
"narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.11",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

53
flake.nix Normal file
View File

@ -0,0 +1,53 @@
{
description = "LEZ Explorer UI - A Qt block explorer for the Logos Execution Zone";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
};
outputs = { self, nixpkgs }:
let
systems = [ "aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux" ];
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f {
pkgs = import nixpkgs { inherit system; };
});
in
{
packages = forAllSystems ({ pkgs }:
let
common = import ./nix/default.nix { inherit pkgs; };
src = ./.;
lib = import ./nix/lib.nix { inherit pkgs common src; };
app = import ./nix/app.nix {
inherit pkgs common src;
lezExplorerUI = lib;
};
in
{
lez-explorer-ui-lib = lib;
app = app;
lib = lib;
default = lib;
}
);
devShells = forAllSystems ({ pkgs }: {
default = pkgs.mkShell {
nativeBuildInputs = [
pkgs.cmake
pkgs.ninja
pkgs.pkg-config
];
buildInputs = [
pkgs.qt6.qtbase
];
shellHook = ''
echo "LEZ Explorer UI development environment"
'';
};
});
};
}

17
interfaces/IComponent.h Normal file
View File

@ -0,0 +1,17 @@
#pragma once
#include <QObject>
#include <QWidget>
#include <QtPlugin>
class LogosAPI;
class IComponent {
public:
virtual ~IComponent() = default;
virtual QWidget* createWidget(LogosAPI* logosAPI = nullptr) = 0;
virtual void destroyWidget(QWidget* widget) = 0;
};
#define IComponent_iid "com.logos.component.IComponent"
Q_DECLARE_INTERFACE(IComponent, IComponent_iid)

10
metadata.json Normal file
View File

@ -0,0 +1,10 @@
{
"name": "lez_explorer_ui",
"version": "1.0.0",
"description": "LEZ Explorer UI - A block explorer for the Logos Execution Zone",
"type": "ui",
"main": "lez_explorer_ui",
"dependencies": [],
"category": "explorer",
"capabilities": ["ui_components"]
}

64
nix/app.nix Normal file
View File

@ -0,0 +1,64 @@
# Builds the lez-explorer-ui-app standalone application
{ pkgs, common, src, lezExplorerUI }:
pkgs.stdenv.mkDerivation rec {
pname = "lez-explorer-ui-app";
version = common.version;
inherit src;
inherit (common) buildInputs meta;
nativeBuildInputs = common.nativeBuildInputs ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.patchelf ];
# This is a GUI application, enable Qt wrapping
dontWrapQtApps = false;
qtLibPath = pkgs.lib.makeLibraryPath ([
pkgs.qt6.qtbase
] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [
pkgs.libglvnd
]);
qtPluginPath = "${pkgs.qt6.qtbase}/lib/qt-6/plugins:${pkgs.qt6.qtsvg}/lib/qt-6/plugins";
qtWrapperArgs = [
"--prefix" "LD_LIBRARY_PATH" ":" qtLibPath
"--prefix" "QT_PLUGIN_PATH" ":" qtPluginPath
];
configurePhase = ''
runHook preConfigure
cmake -S app -B build \
-GNinja \
-DCMAKE_BUILD_TYPE=Release
runHook postConfigure
'';
buildPhase = ''
runHook preBuild
cmake --build build
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p $out/bin $out/lib
# Install the app binary
cp build/bin/lez-explorer-ui-app $out/bin/
# Determine platform-specific plugin extension
OS_EXT="so"
case "$(uname -s)" in
Darwin) OS_EXT="dylib";;
esac
# Copy the explorer UI plugin to root directory (loaded relative to binary)
if [ -f "${lezExplorerUI}/lib/lez_explorer_ui.$OS_EXT" ]; then
cp -L "${lezExplorerUI}/lib/lez_explorer_ui.$OS_EXT" "$out/"
fi
runHook postInstall
'';
}

28
nix/default.nix Normal file
View File

@ -0,0 +1,28 @@
# Common build configuration shared across all packages
{ pkgs }:
{
pname = "lez-explorer-ui";
version = "1.0.0";
nativeBuildInputs = [
pkgs.cmake
pkgs.ninja
pkgs.pkg-config
pkgs.qt6.wrapQtAppsHook
];
buildInputs = [
pkgs.qt6.qtbase
pkgs.qt6.qtsvg
];
cmakeFlags = [
"-GNinja"
];
meta = with pkgs.lib; {
description = "LEZ Explorer UI - A Qt block explorer for the Logos Execution Zone";
platforms = platforms.unix;
};
}

45
nix/lib.nix Normal file
View File

@ -0,0 +1,45 @@
# Builds the lez-explorer-ui library (Qt plugin)
{ pkgs, common, src }:
pkgs.stdenv.mkDerivation {
pname = "${common.pname}-lib";
version = common.version;
inherit src;
inherit (common) buildInputs cmakeFlags meta;
nativeBuildInputs = common.nativeBuildInputs;
# Library (Qt plugin), not an app — no Qt wrapper
dontWrapQtApps = true;
configurePhase = ''
runHook preConfigure
cmake -S . -B build \
-GNinja \
-DCMAKE_BUILD_TYPE=Release
runHook postConfigure
'';
buildPhase = ''
runHook preBuild
cmake --build build
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p $out/lib
if [ -f build/modules/lez_explorer_ui.dylib ]; then
cp build/modules/lez_explorer_ui.dylib $out/lib/
elif [ -f build/modules/lez_explorer_ui.so ]; then
cp build/modules/lez_explorer_ui.so $out/lib/
else
echo "Error: No library file found in build/modules/"
ls -la build/modules/ 2>/dev/null || true
exit 1
fi
runHook postInstall
'';
}

17
src/ExplorerPlugin.cpp Normal file
View File

@ -0,0 +1,17 @@
#include "ExplorerPlugin.h"
#include "ExplorerWidget.h"
ExplorerPlugin::ExplorerPlugin(QObject* parent)
: QObject(parent)
{
}
QWidget* ExplorerPlugin::createWidget(LogosAPI* /*logosAPI*/)
{
return new ExplorerWidget();
}
void ExplorerPlugin::destroyWidget(QWidget* widget)
{
delete widget;
}

18
src/ExplorerPlugin.h Normal file
View File

@ -0,0 +1,18 @@
#pragma once
#include "IComponent.h"
#include <QObject>
#include <QtPlugin>
class ExplorerPlugin : public QObject, public IComponent {
Q_OBJECT
Q_INTERFACES(IComponent)
Q_PLUGIN_METADATA(IID IComponent_iid FILE "metadata.json")
public:
explicit ExplorerPlugin(QObject* parent = nullptr);
Q_INVOKABLE QWidget* createWidget(LogosAPI* logosAPI = nullptr) override;
void destroyWidget(QWidget* widget) override;
};

213
src/ExplorerWidget.cpp Normal file
View File

@ -0,0 +1,213 @@
#include "ExplorerWidget.h"
#include "Style.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 <QVBoxLayout>
#include <QStackedWidget>
#include <QLabel>
ExplorerWidget::ExplorerWidget(QWidget* parent)
: QWidget(parent)
, m_indexer(std::make_unique<MockIndexerService>())
{
setStyleSheet(Style::appBackground());
auto* layout = new QVBoxLayout(this);
layout->setContentsMargins(16, 12, 16, 12);
layout->setSpacing(8);
// Navigation bar
m_navBar = new NavigationBar(this);
layout->addWidget(m_navBar);
// 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);
connect(m_indexer.get(), &IndexerService::newBlockAdded, this, [this](const Block& block) {
m_mainPage->onNewBlock(block);
});
updateNavButtons();
}
ExplorerWidget::~ExplorerWidget() = default;
void ExplorerWidget::connectPageSignals(QWidget* page)
{
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, this);
connectPageSignals(page);
showPage(page);
} else {
qWarning("Block #%llu not found", static_cast<unsigned long long>(t.blockId));
showPage(m_mainPage);
}
} else if constexpr (std::is_same_v<T, NavTransaction>) {
auto tx = m_indexer->getTransaction(t.hash);
if (tx) {
auto* page = new TransactionPage(*tx, this);
connectPageSignals(page);
showPage(page);
} else {
qWarning("Transaction %s not found", qPrintable(t.hash));
showPage(m_mainPage);
}
} 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(), this);
connectPageSignals(page);
showPage(page);
} else {
qWarning("Account %s not found", qPrintable(t.accountId));
showPage(m_mainPage);
}
}
}, 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());
}

49
src/ExplorerWidget.h Normal file
View File

@ -0,0 +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;
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;
};

142
src/Style.h Normal file
View File

@ -0,0 +1,142 @@
#pragma once
#include <QString>
namespace Style {
// --- Color palette ---
namespace Color {
inline const char* bg() { return "#1e1e2e"; }
inline const char* surface() { return "#2a2a3c"; }
inline const char* surfaceHover() { return "#33334a"; }
inline const char* border() { return "#3b3b52"; }
inline const char* text() { return "#cdd6f4"; }
inline const char* subtext() { return "#7f849c"; }
inline const char* accent() { return "#89b4fa"; }
inline const char* green() { return "#a6e3a1"; }
inline const char* yellow() { return "#f9e2af"; }
inline const char* red() { return "#f38ba8"; }
inline const char* blue() { return "#89b4fa"; }
inline const char* mauve() { return "#cba6f7"; }
inline const char* peach() { return "#fab387"; }
inline const char* teal() { return "#94e2d5"; }
}
// --- Base font size applied to the entire app ---
inline int baseFontSize() { return 14; }
// --- Widget styles ---
inline QString appBackground()
{
return QString("background-color: %1; color: %2; font-size: %3px;")
.arg(Color::bg(), Color::text())
.arg(baseFontSize());
}
inline QString cardFrame()
{
return QString(
"background: %1; border: 1px solid %2; border-radius: 8px; padding: 18px;"
).arg(Color::surface(), Color::border());
}
inline QString cardFrameWithLabels()
{
return QString(
"QFrame { background: %1; border: 1px solid %2; border-radius: 8px; padding: 18px; }"
" QFrame QLabel { color: %3; font-size: %4px; }"
).arg(Color::surface(), Color::border(), Color::text())
.arg(baseFontSize());
}
inline QString clickableRowWithLabels(const QString& selector)
{
return QString(
"%1 { background: %2; border: 1px solid %3; border-radius: 8px; padding: 12px 16px; margin: 3px 0; }"
" %1 QLabel { color: %4; font-size: %5px; }"
).arg(selector, Color::surface(), Color::border(), Color::text())
.arg(baseFontSize());
}
inline QString mutedText()
{
return QString("color: %1;").arg(Color::subtext());
}
inline QString monoText()
{
return QString("font-family: 'Menlo', 'Courier New'; font-size: %1px; color: %2;")
.arg(baseFontSize() - 1)
.arg(Color::accent());
}
inline QString badge(const QString& bgColor)
{
return QString("color: %1; background: %2; border-radius: 4px; padding: 3px 12px; font-weight: bold; font-size: %3px;")
.arg(Color::bg(), bgColor)
.arg(baseFontSize() - 1);
}
inline QString sectionHeader()
{
return QString("color: %1;").arg(Color::text());
}
inline QString navButton()
{
return QString(
"QPushButton { background: %1; color: %2; border: 1px solid %3; border-radius: 6px; padding: 6px 14px; font-size: %7px; }"
" QPushButton:hover { background: %4; border-color: %5; }"
" QPushButton:disabled { color: %6; background: %1; border-color: %3; opacity: 0.4; }"
).arg(Color::surface(), Color::text(), Color::border(), Color::surfaceHover(), Color::accent(), Color::subtext())
.arg(baseFontSize() + 2);
}
inline QString searchInput()
{
return QString(
"QLineEdit { background: %1; color: %2; border: 1px solid %3; border-radius: 6px; padding: 10px 14px; font-size: %6px; }"
" QLineEdit:focus { border-color: %4; }"
" QLineEdit::placeholder { color: %5; }"
).arg(Color::surface(), Color::text(), Color::border(), Color::accent(), Color::subtext())
.arg(baseFontSize());
}
inline QString searchButton()
{
return QString(
"QPushButton { background: %1; color: %2; border: none; border-radius: 6px; padding: 10px 20px; font-size: %4px; font-weight: bold; }"
" QPushButton:hover { background: %3; }"
).arg(Color::accent(), Color::bg(), Color::mauve())
.arg(baseFontSize());
}
inline QString healthLabel()
{
return QString("color: %1; font-weight: bold; font-size: %2px;")
.arg(Color::green())
.arg(baseFontSize());
}
// --- Status colors ---
inline QString statusColor(const QString& status)
{
if (status == "Finalized") return Color::green();
if (status == "Safe") return Color::yellow();
return Color::subtext(); // Pending
}
// --- Transaction type colors ---
inline QString txTypeColor(const QString& type)
{
if (type == "Public") return Color::blue();
if (type == "Privacy-Preserving") return Color::mauve();
return Color::peach(); // Program Deployment
}
} // namespace Style

View File

@ -0,0 +1,12 @@
<RCC>
<qresource prefix="/">
<file>icons/arrow-left.svg</file>
<file>icons/arrow-right.svg</file>
<file>icons/home.svg</file>
<file>icons/search.svg</file>
<file>icons/box.svg</file>
<file>icons/file-text.svg</file>
<file>icons/user.svg</file>
<file>icons/activity.svg</file>
</qresource>
</RCC>

3
src/icons/activity.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#a6e3a1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
</svg>

After

Width:  |  Height:  |  Size: 216 B

4
src/icons/arrow-left.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="19" y1="12" x2="5" y2="12"/>
<polyline points="12 19 5 12 12 5"/>
</svg>

After

Width:  |  Height:  |  Size: 241 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="5" y1="12" x2="19" y2="12"/>
<polyline points="12 5 19 12 12 19"/>
</svg>

After

Width:  |  Height:  |  Size: 242 B

5
src/icons/box.svg Normal file
View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#89b4fa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
<line x1="12" y1="22.08" x2="12" y2="12"/>
</svg>

After

Width:  |  Height:  |  Size: 395 B

7
src/icons/file-text.svg Normal file
View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#89b4fa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>

After

Width:  |  Height:  |  Size: 390 B

4
src/icons/home.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>

After

Width:  |  Height:  |  Size: 267 B

4
src/icons/search.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"/>
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>

After

Width:  |  Height:  |  Size: 243 B

4
src/icons/user.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#89b4fa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>

After

Width:  |  Height:  |  Size: 250 B

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

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

@ -0,0 +1,160 @@
#include "AccountPage.h"
#include "Style.h"
#include "widgets/ClickableFrame.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QScrollArea>
#include <QFrame>
#include <QGridLayout>
#include <QIcon>
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);
outerLayout->setContentsMargins(0, 0, 0, 0);
auto* scrollArea = new QScrollArea(this);
scrollArea->setWidgetResizable(true);
scrollArea->setFrameShape(QFrame::NoFrame);
scrollArea->setStyleSheet("QScrollArea { background: transparent; border: none; } QWidget#scrollContent { background: transparent; }");
auto* scrollContent = new QWidget();
scrollContent->setObjectName("scrollContent");
auto* layout = new QVBoxLayout(scrollContent);
layout->setAlignment(Qt::AlignTop);
layout->setContentsMargins(0, 0, 0, 0);
// Title with icon
auto* titleRow = new QHBoxLayout();
titleRow->setSpacing(10);
auto* titleIcon = new QLabel();
titleIcon->setPixmap(QIcon(":/icons/user.svg").pixmap(30, 30));
auto* title = new QLabel("Account Details");
QFont titleFont = title->font();
titleFont.setPointSize(24);
titleFont.setBold(true);
title->setFont(titleFont);
title->setStyleSheet(QString("color: %1;").arg(Style::Color::text()));
titleRow->addWidget(titleIcon);
titleRow->addWidget(title);
titleRow->addStretch();
layout->addLayout(titleRow);
layout->addSpacing(8);
// Account info grid
auto* infoFrame = new QFrame();
infoFrame->setFrameShape(QFrame::NoFrame);
infoFrame->setStyleSheet(Style::cardFrameWithLabels());
auto* grid = new QGridLayout(infoFrame);
grid->setColumnStretch(1, 1);
grid->setVerticalSpacing(10);
grid->setHorizontalSpacing(20);
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);
auto* balVal = makeValueLabel(account.balance);
balVal->setStyleSheet(QString("color: %1; font-weight: bold;").arg(Style::Color::green()));
grid->addWidget(balVal, 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* headerRow = new QHBoxLayout();
headerRow->setContentsMargins(0, 16, 0, 6);
headerRow->setSpacing(8);
auto* headerIcon = new QLabel();
headerIcon->setPixmap(QIcon(":/icons/file-text.svg").pixmap(24, 24));
auto* txHeader = new QLabel("Transaction History");
QFont headerFont = txHeader->font();
headerFont.setPointSize(20);
headerFont.setBold(true);
txHeader->setFont(headerFont);
txHeader->setStyleSheet(Style::sectionHeader());
headerRow->addWidget(headerIcon);
headerRow->addWidget(txHeader);
headerRow->addStretch();
layout->addLayout(headerRow);
for (const auto& tx : transactions) {
auto* frame = new ClickableFrame();
frame->setFrameShape(QFrame::NoFrame);
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
auto* txRow = new QHBoxLayout(frame);
txRow->setSpacing(10);
auto* icon = new QLabel();
icon->setPixmap(QIcon(":/icons/file-text.svg").pixmap(20, 20));
txRow->addWidget(icon);
auto* hashLabel = new QLabel(tx.hash);
QFont boldFont = hashLabel->font();
boldFont.setBold(true);
hashLabel->setFont(boldFont);
QString typeStr = transactionTypeToString(tx.type);
auto* typeLabel = new QLabel(typeStr);
typeLabel->setStyleSheet(Style::badge(Style::txTypeColor(typeStr)));
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);
};

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

@ -0,0 +1,218 @@
#include "BlockPage.h"
#include "Style.h"
#include "widgets/ClickableFrame.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QScrollArea>
#include <QFrame>
#include <QGridLayout>
#include <QIcon>
#include <QEvent>
#include <QMouseEvent>
namespace {
class PrevHashClickFilter : public QObject {
public:
PrevHashClickFilter(quint64 blockId, BlockPage* page, QObject* parent)
: QObject(parent), m_blockId(blockId), m_page(page) {}
protected:
bool eventFilter(QObject* obj, QEvent* event) override {
if (event->type() == QEvent::MouseButtonRelease) {
auto* me = static_cast<QMouseEvent*>(event);
if (me->button() == Qt::LeftButton) {
emit m_page->blockClicked(m_blockId);
return true;
}
}
return QObject::eventFilter(obj, event);
}
private:
quint64 m_blockId;
BlockPage* m_page;
};
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);
outerLayout->setContentsMargins(0, 0, 0, 0);
auto* scrollArea = new QScrollArea(this);
scrollArea->setWidgetResizable(true);
scrollArea->setFrameShape(QFrame::NoFrame);
scrollArea->setStyleSheet("QScrollArea { background: transparent; border: none; } QWidget#scrollContent { background: transparent; }");
auto* scrollContent = new QWidget();
scrollContent->setObjectName("scrollContent");
auto* layout = new QVBoxLayout(scrollContent);
layout->setAlignment(Qt::AlignTop);
layout->setContentsMargins(0, 0, 0, 0);
// Title with icon
auto* titleRow = new QHBoxLayout();
titleRow->setSpacing(10);
auto* titleIcon = new QLabel();
titleIcon->setPixmap(QIcon(":/icons/box.svg").pixmap(30, 30));
auto* title = new QLabel(QString("Block #%1").arg(block.blockId));
QFont titleFont = title->font();
titleFont.setPointSize(24);
titleFont.setBold(true);
title->setFont(titleFont);
title->setStyleSheet(QString("color: %1;").arg(Style::Color::text()));
titleRow->addWidget(titleIcon);
titleRow->addWidget(title);
titleRow->addStretch();
layout->addLayout(titleRow);
layout->addSpacing(8);
// Block info grid
auto* infoFrame = new QFrame();
infoFrame->setFrameShape(QFrame::NoFrame);
infoFrame->setStyleSheet(Style::cardFrameWithLabels());
auto* grid = new QGridLayout(infoFrame);
grid->setColumnStretch(1, 1);
grid->setVerticalSpacing(10);
grid->setHorizontalSpacing(20);
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);
auto* hashVal = makeValueLabel(block.hash);
hashVal->setStyleSheet(Style::monoText());
grid->addWidget(hashVal, row++, 1);
grid->addWidget(makeFieldLabel("Previous Hash"), row, 0);
if (block.blockId > 1) {
auto* prevHashLabel = new QLabel(block.prevBlockHash);
prevHashLabel->setStyleSheet(QString("color: %1; text-decoration: underline;").arg(Style::Color::accent()));
prevHashLabel->setCursor(Qt::PointingHandCursor);
prevHashLabel->setWordWrap(true);
prevHashLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
quint64 prevBlockId = block.blockId - 1;
prevHashLabel->installEventFilter(new PrevHashClickFilter(prevBlockId, this, this));
grid->addWidget(prevHashLabel, row++, 1);
} else {
grid->addWidget(makeValueLabel(block.prevBlockHash), 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 statusStr = bedrockStatusToString(block.bedrockStatus);
auto* statusLabel = new QLabel(statusStr);
statusLabel->setStyleSheet(Style::badge(Style::statusColor(statusStr)));
statusLabel->setMaximumWidth(120);
grid->addWidget(statusLabel, row++, 1);
grid->addWidget(makeFieldLabel("Signature"), row, 0);
// Insert zero-width spaces every 32 chars so QLabel can word-wrap the long hex string
QString wrappableSig = block.signature;
for (int i = 32; i < wrappableSig.size(); i += 33) {
wrappableSig.insert(i, QChar(0x200B));
}
auto* sigLabel = new QLabel(wrappableSig);
sigLabel->setStyleSheet(Style::monoText());
sigLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
sigLabel->setWordWrap(true);
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* headerRow = new QHBoxLayout();
headerRow->setContentsMargins(0, 16, 0, 6);
headerRow->setSpacing(8);
auto* headerIcon = new QLabel();
headerIcon->setPixmap(QIcon(":/icons/file-text.svg").pixmap(24, 24));
auto* txHeader = new QLabel("Transactions");
QFont headerFont = txHeader->font();
headerFont.setPointSize(20);
headerFont.setBold(true);
txHeader->setFont(headerFont);
txHeader->setStyleSheet(Style::sectionHeader());
headerRow->addWidget(headerIcon);
headerRow->addWidget(txHeader);
headerRow->addStretch();
layout->addLayout(headerRow);
for (const auto& tx : block.transactions) {
auto* frame = new ClickableFrame();
frame->setFrameShape(QFrame::NoFrame);
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
auto* txRow = new QHBoxLayout(frame);
txRow->setSpacing(10);
auto* icon = new QLabel();
icon->setPixmap(QIcon(":/icons/file-text.svg").pixmap(20, 20));
txRow->addWidget(icon);
auto* hashLabel = new QLabel(tx.hash);
QFont boldFont = hashLabel->font();
boldFont.setBold(true);
hashLabel->setFont(boldFont);
QString typeStr = transactionTypeToString(tx.type);
auto* typeLabel = new QLabel(typeStr);
typeLabel->setStyleSheet(Style::badge(Style::txTypeColor(typeStr)));
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);
};

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

@ -0,0 +1,347 @@
#include "MainPage.h"
#include "Style.h"
#include "widgets/ClickableFrame.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QScrollArea>
#include <QFrame>
#include <QPixmap>
#include <QPushButton>
MainPage::MainPage(IndexerService* indexer, QWidget* parent)
: QWidget(parent)
, m_indexer(indexer)
{
auto* outerLayout = new QVBoxLayout(this);
outerLayout->setContentsMargins(0, 0, 0, 0);
// Health indicator
auto* healthRow = new QHBoxLayout();
auto* healthIcon = new QLabel(this);
healthIcon->setPixmap(QIcon(":/icons/activity.svg").pixmap(20, 20));
m_healthLabel = new QLabel(this);
healthRow->addWidget(healthIcon);
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);
scrollArea->setStyleSheet("QScrollArea { background: transparent; border: none; } QWidget#scrollContent { background: transparent; }");
auto* scrollContent = new QWidget();
scrollContent->setObjectName("scrollContent");
m_contentLayout = new QVBoxLayout(scrollContent);
m_contentLayout->setAlignment(Qt::AlignTop);
m_contentLayout->setContentsMargins(0, 0, 0, 0);
scrollArea->setWidget(scrollContent);
outerLayout->addWidget(scrollArea);
refresh();
}
QWidget* MainPage::createSectionHeader(const QString& title, const QString& iconPath)
{
auto* container = new QWidget();
auto* layout = new QHBoxLayout(container);
layout->setContentsMargins(0, 12, 0, 6);
layout->setSpacing(8);
layout->setAlignment(Qt::AlignVCenter);
if (!iconPath.isEmpty()) {
auto* icon = new QLabel();
icon->setPixmap(QIcon(iconPath).pixmap(24, 24));
icon->setFixedSize(24, 24);
layout->addWidget(icon);
}
auto* label = new QLabel(title);
QFont font = label->font();
font.setPointSize(20);
font.setBold(true);
label->setFont(font);
label->setStyleSheet(Style::sectionHeader());
layout->addWidget(label);
layout->addStretch();
return container;
}
void MainPage::addBlockRow(QVBoxLayout* layout, const Block& block)
{
auto* frame = new ClickableFrame();
frame->setFrameShape(QFrame::NoFrame);
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
auto* row = new QHBoxLayout(frame);
row->setSpacing(10);
auto* icon = new QLabel();
icon->setPixmap(QIcon(":/icons/box.svg").pixmap(20, 20));
row->addWidget(icon);
auto* idLabel = new QLabel(QString("Block #%1").arg(block.blockId));
QFont boldFont = idLabel->font();
boldFont.setBold(true);
idLabel->setFont(boldFont);
QString statusStr = bedrockStatusToString(block.bedrockStatus);
auto* statusLabel = new QLabel(statusStr);
statusLabel->setStyleSheet(Style::badge(Style::statusColor(statusStr)));
auto* hashLabel = new QLabel(block.hash);
hashLabel->setStyleSheet(Style::mutedText() + " font-family: 'Menlo', 'Courier New'; font-size: 11px;");
hashLabel->setFixedWidth(470);
auto* txIcon = new QLabel();
txIcon->setPixmap(QIcon(":/icons/file-text.svg").pixmap(18, 18));
auto* txCount = new QLabel(QString::number(block.transactions.size()));
auto* timeLabel = new QLabel(block.timestamp.toString("yyyy-MM-dd hh:mm:ss UTC"));
timeLabel->setStyleSheet(Style::mutedText());
statusLabel->setFixedWidth(90);
statusLabel->setAlignment(Qt::AlignCenter);
row->addWidget(idLabel);
row->addWidget(hashLabel);
row->addStretch(1);
row->addWidget(txIcon);
row->addWidget(txCount);
row->addWidget(timeLabel);
row->addWidget(statusLabel);
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::NoFrame);
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
auto* row = new QHBoxLayout(frame);
row->setSpacing(10);
auto* icon = new QLabel();
icon->setPixmap(QIcon(":/icons/file-text.svg").pixmap(20, 20));
row->addWidget(icon);
auto* hashLabel = new QLabel(tx.hash);
QFont boldFont = hashLabel->font();
boldFont.setBold(true);
hashLabel->setFont(boldFont);
QString typeStr = transactionTypeToString(tx.type);
auto* typeLabel = new QLabel(typeStr);
typeLabel->setStyleSheet(Style::badge(Style::txTypeColor(typeStr)));
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::NoFrame);
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
auto* row = new QHBoxLayout(frame);
row->setSpacing(10);
auto* icon = new QLabel();
icon->setPixmap(QIcon(":/icons/user.svg").pixmap(20, 20));
row->addWidget(icon);
auto* idLabel = new QLabel(account.accountId);
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(Style::healthLabel());
if (m_recentBlocksWidget) {
m_contentLayout->removeWidget(m_recentBlocksWidget);
m_recentBlocksWidget->deleteLater();
m_recentBlocksWidget = nullptr;
m_blocksLayout = nullptr;
m_loadMoreBtn = nullptr;
}
m_oldestLoadedBlockId = std::nullopt;
m_recentBlocksWidget = new QWidget();
auto* outerLayout = new QVBoxLayout(m_recentBlocksWidget);
outerLayout->setContentsMargins(0, 0, 0, 0);
outerLayout->addWidget(createSectionHeader("Recent Blocks", ":/icons/box.svg"));
// Dedicated layout for block rows — button is appended after this in outerLayout
auto* blockRowsWidget = new QWidget();
m_blocksLayout = new QVBoxLayout(blockRowsWidget);
m_blocksLayout->setContentsMargins(0, 0, 0, 0);
outerLayout->addWidget(blockRowsWidget);
auto blocks = m_indexer->getBlocks(std::nullopt, 10);
for (const auto& block : blocks) {
addBlockRow(m_blocksLayout, block);
if (!m_oldestLoadedBlockId || block.blockId < *m_oldestLoadedBlockId)
m_oldestLoadedBlockId = block.blockId;
}
m_loadMoreBtn = new QPushButton("Load more");
m_loadMoreBtn->setStyleSheet(Style::searchButton());
m_loadMoreBtn->setVisible(m_oldestLoadedBlockId && *m_oldestLoadedBlockId > 1);
connect(m_loadMoreBtn, &QPushButton::clicked, this, &MainPage::loadMoreBlocks);
outerLayout->addWidget(m_loadMoreBtn, 0, Qt::AlignHCenter);
m_contentLayout->addWidget(m_recentBlocksWidget);
}
void MainPage::loadMoreBlocks()
{
if (!m_oldestLoadedBlockId || *m_oldestLoadedBlockId <= 1)
return;
auto blocks = m_indexer->getBlocks(m_oldestLoadedBlockId, 10);
for (const auto& block : blocks) {
addBlockRow(m_blocksLayout, block);
if (block.blockId < *m_oldestLoadedBlockId)
m_oldestLoadedBlockId = block.blockId;
}
m_loadMoreBtn->setVisible(*m_oldestLoadedBlockId > 1 && !blocks.isEmpty());
}
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", ":/icons/box.svg"));
for (const auto& block : results.blocks) {
addBlockRow(layout, block);
}
hasResults = true;
}
if (!results.transactions.isEmpty()) {
layout->addWidget(createSectionHeader("Transactions", ":/icons/file-text.svg"));
for (const auto& tx : results.transactions) {
addTransactionRow(layout, tx);
}
hasResults = true;
}
if (!results.accounts.isEmpty()) {
layout->addWidget(createSectionHeader("Accounts", ":/icons/user.svg"));
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::onNewBlock(const Block& block)
{
if (!m_blocksLayout) return;
m_healthLabel->setText(QString("Chain height: %1").arg(block.blockId));
// Append to layout then move to top (index 0)
int countBefore = m_blocksLayout->count();
addBlockRow(m_blocksLayout, block);
if (m_blocksLayout->count() <= countBefore) return;
QLayoutItem* item = m_blocksLayout->itemAt(m_blocksLayout->count() - 1);
if (!item || !item->widget()) return;
QWidget* newRow = item->widget();
m_blocksLayout->removeWidget(newRow);
m_blocksLayout->insertWidget(0, newRow);
}
void MainPage::clearSearchResults()
{
if (m_searchResultsWidget) {
m_contentLayout->removeWidget(m_searchResultsWidget);
m_searchResultsWidget->deleteLater();
m_searchResultsWidget = nullptr;
}
if (m_recentBlocksWidget) {
m_recentBlocksWidget->show();
}
}

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

@ -0,0 +1,43 @@
#pragma once
#include "services/IndexerService.h"
#include <QWidget>
#include <optional>
class QVBoxLayout;
class QLabel;
class QPushButton;
class MainPage : public QWidget {
Q_OBJECT
public:
explicit MainPage(IndexerService* indexer, QWidget* parent = nullptr);
void refresh();
void showSearchResults(const SearchResults& results);
void clearSearchResults();
void onNewBlock(const Block& block);
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, const QString& iconPath = {});
void loadMoreBlocks();
IndexerService* m_indexer = nullptr;
QVBoxLayout* m_contentLayout = nullptr;
QWidget* m_searchResultsWidget = nullptr;
QWidget* m_recentBlocksWidget = nullptr;
QVBoxLayout* m_blocksLayout = nullptr;
QPushButton* m_loadMoreBtn = nullptr;
QLabel* m_healthLabel = nullptr;
std::optional<quint64> m_oldestLoadedBlockId;
};

View File

@ -0,0 +1,185 @@
#include "TransactionPage.h"
#include "Style.h"
#include "widgets/ClickableFrame.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QScrollArea>
#include <QFrame>
#include <QGridLayout>
#include <QIcon>
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);
outerLayout->setContentsMargins(0, 0, 0, 0);
auto* scrollArea = new QScrollArea(this);
scrollArea->setWidgetResizable(true);
scrollArea->setFrameShape(QFrame::NoFrame);
scrollArea->setStyleSheet("QScrollArea { background: transparent; border: none; } QWidget#scrollContent { background: transparent; }");
auto* scrollContent = new QWidget();
scrollContent->setObjectName("scrollContent");
auto* layout = new QVBoxLayout(scrollContent);
layout->setAlignment(Qt::AlignTop);
layout->setContentsMargins(0, 0, 0, 0);
// Title with icon
auto* titleRow = new QHBoxLayout();
titleRow->setSpacing(10);
auto* titleIcon = new QLabel();
titleIcon->setPixmap(QIcon(":/icons/file-text.svg").pixmap(30, 30));
auto* title = new QLabel("Transaction Details");
QFont titleFont = title->font();
titleFont.setPointSize(24);
titleFont.setBold(true);
title->setFont(titleFont);
title->setStyleSheet(QString("color: %1;").arg(Style::Color::text()));
titleRow->addWidget(titleIcon);
titleRow->addWidget(title);
titleRow->addStretch();
layout->addLayout(titleRow);
layout->addSpacing(8);
// Transaction info grid
auto* infoFrame = new QFrame();
infoFrame->setFrameShape(QFrame::NoFrame);
infoFrame->setStyleSheet(Style::cardFrameWithLabels());
auto* grid = new QGridLayout(infoFrame);
grid->setColumnStretch(1, 1);
grid->setVerticalSpacing(10);
grid->setHorizontalSpacing(20);
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 typeStr = transactionTypeToString(tx.type);
auto* typeLabel = new QLabel(typeStr);
typeLabel->setStyleSheet(Style::badge(Style::txTypeColor(typeStr)));
typeLabel->setMaximumWidth(180);
grid->addWidget(typeLabel, row++, 1);
// Type-specific fields
switch (tx.type) {
case TransactionType::Public:
grid->addWidget(makeFieldLabel("Program ID"), row, 0);
{ auto* v = makeValueLabel(tx.programId); v->setStyleSheet(Style::monoText()); grid->addWidget(v, 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* headerRow = new QHBoxLayout();
headerRow->setContentsMargins(0, 16, 0, 6);
headerRow->setSpacing(8);
auto* headerIcon = new QLabel();
headerIcon->setPixmap(QIcon(":/icons/user.svg").pixmap(24, 24));
auto* accHeader = new QLabel("Accounts");
QFont headerFont = accHeader->font();
headerFont.setPointSize(20);
headerFont.setBold(true);
accHeader->setFont(headerFont);
accHeader->setStyleSheet(Style::sectionHeader());
headerRow->addWidget(headerIcon);
headerRow->addWidget(accHeader);
headerRow->addStretch();
layout->addLayout(headerRow);
for (const auto& accRef : tx.accounts) {
auto* frame = new ClickableFrame();
frame->setFrameShape(QFrame::NoFrame);
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
auto* accRow = new QHBoxLayout(frame);
accRow->setSpacing(10);
auto* icon = new QLabel();
icon->setPixmap(QIcon(":/icons/user.svg").pixmap(20, 20));
accRow->addWidget(icon);
auto* idLabel = new QLabel(accRef.accountId);
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,35 @@
#pragma once
#include "models/Block.h"
#include "models/Transaction.h"
#include "models/Account.h"
#include <QObject>
#include <QVector>
#include <optional>
struct SearchResults {
QVector<Block> blocks;
QVector<Transaction> transactions;
QVector<Account> accounts;
};
class IndexerService : public QObject {
Q_OBJECT
public:
explicit IndexerService(QObject* parent = nullptr) : QObject(parent) {}
~IndexerService() override = 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;
signals:
void newBlockAdded(Block block);
};

View File

@ -0,0 +1,367 @@
#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(QObject* parent)
: IndexerService(parent)
{
generateData();
connect(&m_blockTimer, &QTimer::timeout, this, &MockIndexerService::onGenerateBlock);
m_blockTimer.start(30000); // 30 seconds
}
Block MockIndexerService::generateBlock(quint64 blockId, const QString& prevHash)
{
auto* rng = QRandomGenerator::global();
Block block;
block.blockId = blockId;
block.prevBlockHash = prevHash;
block.hash = randomHash();
block.timestamp = QDateTime::currentDateTimeUtc();
block.signature = randomHexString(64);
block.bedrockParentId = randomHexString(32);
block.bedrockStatus = BedrockStatus::Pending;
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;
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;
}
}
}
return block;
}
void MockIndexerService::onGenerateBlock()
{
quint64 newId = m_blocks.isEmpty() ? 1 : m_blocks.last().blockId + 1;
QString prevHash = m_blocks.isEmpty() ? QString(64, '0') : m_blocks.last().hash;
Block block = generateBlock(newId, prevHash);
m_blocks.append(block);
m_blocksByHash[block.hash] = block;
emit newBlockAdded(block);
}
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,40 @@
#pragma once
#include "IndexerService.h"
#include <QMap>
#include <QTimer>
class MockIndexerService : public IndexerService {
Q_OBJECT
public:
explicit MockIndexerService(QObject* parent = nullptr);
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 slots:
void onGenerateBlock();
private:
void generateData();
QString randomHash();
QString randomAccountId();
Transaction generatePublicTransaction();
Transaction generatePrivacyPreservingTransaction();
Transaction generateProgramDeploymentTransaction();
Block generateBlock(quint64 blockId, const QString& prevHash);
QVector<Block> m_blocks;
QMap<QString, Block> m_blocksByHash;
QMap<QString, Transaction> m_transactionsByHash;
QMap<QString, Account> m_accounts;
QTimer m_blockTimer;
};

View File

@ -0,0 +1,47 @@
#pragma once
#include "Style.h"
#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);
}
void enterEvent(QEnterEvent* event) override
{
m_baseStyleSheet = styleSheet();
setStyleSheet(m_baseStyleSheet +
QString(" ClickableFrame { background: %1 !important; border-color: %2 !important; }")
.arg(Style::Color::surfaceHover(), Style::Color::accent()));
QFrame::enterEvent(event);
}
void leaveEvent(QEvent* event) override
{
setStyleSheet(m_baseStyleSheet);
QFrame::leaveEvent(event);
}
private:
QString m_baseStyleSheet;
};

View File

@ -0,0 +1,63 @@
#include "NavigationBar.h"
#include "Style.h"
#include <QHBoxLayout>
#include <QPushButton>
#include <QLabel>
#include <QIcon>
NavigationBar::NavigationBar(QWidget* parent)
: QWidget(parent)
{
auto* layout = new QHBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(6);
const QSize iconSize(22, 22);
m_backBtn = new QPushButton(this);
m_backBtn->setIcon(QIcon(":/icons/arrow-left.svg"));
m_backBtn->setIconSize(iconSize);
m_backBtn->setFixedSize(42, 42);
m_backBtn->setEnabled(false);
m_backBtn->setToolTip("Back");
m_backBtn->setStyleSheet(Style::navButton());
m_forwardBtn = new QPushButton(this);
m_forwardBtn->setIcon(QIcon(":/icons/arrow-right.svg"));
m_forwardBtn->setIconSize(iconSize);
m_forwardBtn->setFixedSize(42, 42);
m_forwardBtn->setEnabled(false);
m_forwardBtn->setToolTip("Forward");
m_forwardBtn->setStyleSheet(Style::navButton());
auto* homeBtn = new QPushButton(this);
homeBtn->setIcon(QIcon(":/icons/home.svg"));
homeBtn->setIconSize(iconSize);
homeBtn->setFixedSize(42, 42);
homeBtn->setToolTip("Home");
homeBtn->setStyleSheet(Style::navButton());
auto* titleLabel = new QLabel("LEZ Explorer");
titleLabel->setStyleSheet(QString("color: %1; font-size: 14px; font-weight: bold; margin-left: 8px;").arg(Style::Color::accent()));
layout->addWidget(m_backBtn);
layout->addWidget(m_forwardBtn);
layout->addWidget(homeBtn);
layout->addWidget(titleLabel);
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;
};

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

@ -0,0 +1,38 @@
#include "SearchBar.h"
#include "Style.h"
#include <QHBoxLayout>
#include <QLineEdit>
#include <QPushButton>
SearchBar::SearchBar(QWidget* parent)
: QWidget(parent)
{
auto* layout = new QHBoxLayout(this);
layout->setContentsMargins(0, 4, 0, 4);
layout->setSpacing(8);
m_input = new QLineEdit(this);
m_input->setPlaceholderText("Search by block ID / block hash / tx hash / account ID...");
m_input->setMinimumHeight(38);
m_input->setStyleSheet(Style::searchInput());
auto* searchBtn = new QPushButton("Search", this);
searchBtn->setMinimumHeight(38);
searchBtn->setStyleSheet(Style::searchButton());
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;
};