mirror of
https://github.com/logos-blockchain/lez-explorer-ui.git
synced 2026-07-30 03:13:25 +00:00
use logos protocol with indexer dependency
This commit is contained in:
parent
8ea8574778
commit
f4c180ccdc
43
.github/pull_request_template.md
vendored
Normal file
43
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
## 🎯 Purpose
|
||||
|
||||
*What problem does this PR solve or what feature does it add? Mention issues related to it*
|
||||
|
||||
TO COMPLETE
|
||||
|
||||
## ⚙️ Approach
|
||||
|
||||
*Describe the core changes introduced by this PR.*
|
||||
|
||||
TO COMPLETE
|
||||
|
||||
- [ ] Change ...
|
||||
- [ ] Add ...
|
||||
- [ ] Fix ...
|
||||
- [ ] ...
|
||||
|
||||
## 🧪 How to Test
|
||||
|
||||
*How to verify that this PR works as intended.*
|
||||
|
||||
TO COMPLETE
|
||||
|
||||
## 🔗 Dependencies
|
||||
|
||||
*Link PRs that must be merged before this one.*
|
||||
|
||||
TO COMPLETE IF APPLICABLE
|
||||
|
||||
## 🔜 Future Work
|
||||
|
||||
*List any work intentionally left out of this PR, whether due to scope, prioritization, or pending decisions.*
|
||||
|
||||
TO COMPLETE IF APPLICABLE
|
||||
|
||||
## 📋 PR Completion Checklist
|
||||
|
||||
*Mark only completed items. A complete PR should have all boxes ticked.*
|
||||
|
||||
- [ ] Complete PR description
|
||||
- [ ] Implement the core functionality
|
||||
- [ ] Add/update tests
|
||||
- [ ] Add/update documentation and inline comments
|
||||
164
CMakeLists.txt
164
CMakeLists.txt
@ -1,116 +1,68 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(LEZExplorerUIPlugin VERSION 1.0.0 LANGUAGES CXX)
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(LezExplorerUiPlugin LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
# Hand-written Q_OBJECT classes (widgets/pages/services) need MOC;
|
||||
# explorer_resources.qrc -> compiled icons via AUTORCC.
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
|
||||
# Find Qt packages
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Widgets Svg SvgWidgets Network WebSockets)
|
||||
|
||||
# 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/RpcIndexerService.cpp
|
||||
src/services/RpcIndexerService.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
|
||||
Qt6::Network
|
||||
Qt6::WebSockets
|
||||
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"
|
||||
)
|
||||
# Logos Module CMake helper. For nix builds this is provided via
|
||||
# LOGOS_MODULE_BUILDER_ROOT.
|
||||
if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT})
|
||||
include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake)
|
||||
elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/LogosModule.cmake")
|
||||
include(cmake/LogosModule.cmake)
|
||||
else()
|
||||
set_target_properties(lez_explorer_ui PROPERTIES
|
||||
INSTALL_RPATH "$ORIGIN"
|
||||
INSTALL_RPATH_USE_LINK_PATH FALSE
|
||||
)
|
||||
message(FATAL_ERROR "LogosModule.cmake not found. Set LOGOS_MODULE_BUILDER_ROOT.")
|
||||
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
|
||||
# Legacy `type: ui` QWidget plugin (hand-written ExplorerPlugin / IComponent —
|
||||
# no `interface: universal`, so no codegen). The builder links the logos
|
||||
# SDK/protocol (logos_api.h / getClient) automatically; we add the Qt widget
|
||||
# stack the explorer needs on top of the default Core/RemoteObjects.
|
||||
logos_module(
|
||||
NAME lez_explorer_ui
|
||||
SOURCES
|
||||
src/ExplorerPlugin.cpp
|
||||
src/ExplorerPlugin.h
|
||||
src/ExplorerWidget.cpp
|
||||
src/ExplorerWidget.h
|
||||
src/Style.h
|
||||
src/services/IndexerService.h
|
||||
src/services/LpIndexerService.cpp
|
||||
src/services/LpIndexerService.h
|
||||
src/services/MockIndexerService.cpp
|
||||
src/services/MockIndexerService.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
|
||||
FIND_PACKAGES
|
||||
Qt6Widgets
|
||||
Qt6Svg
|
||||
Qt6SvgWidgets
|
||||
Qt6Network
|
||||
LINK_LIBRARIES
|
||||
Qt6::Widgets
|
||||
Qt6::Svg
|
||||
Qt6::SvgWidgets
|
||||
Qt6::Network
|
||||
INCLUDE_DIRS
|
||||
src
|
||||
interfaces
|
||||
)
|
||||
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/metadata.json
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/lez-explorer-ui
|
||||
)
|
||||
|
||||
message(STATUS "LEZ Explorer UI Plugin configured successfully")
|
||||
|
||||
19537
flake.lock
generated
19537
flake.lock
generated
File diff suppressed because it is too large
Load Diff
69
flake.nix
69
flake.nix
@ -1,61 +1,22 @@
|
||||
{
|
||||
description = "LEZ Explorer UI - A Qt block explorer for the Logos Execution Zone";
|
||||
description = "LEZ Explorer UI - a Qt block explorer for the Logos Execution Zone";
|
||||
|
||||
inputs = {
|
||||
# Pinned to the EXACT nixpkgs revision basecamp resolves via logos-nix, so this
|
||||
# in-process `type: ui` module links the identical qtbase (6.9.2) and does not
|
||||
# load a second QtCore into basecamp's process (which aborts with "Must construct
|
||||
# a QApplication before a QWidget"). Matching the version string is not enough —
|
||||
# the store path must match, so this must be re-pinned whenever basecamp bumps its
|
||||
# logos-nix/nixpkgs lock.
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/e9f00bd893984bc8ce46c895c3bf7cac95331127";
|
||||
logos-module-builder.url = "github:logos-co/logos-module-builder";
|
||||
|
||||
# The provider it reads from, over the Logos protocol. Input name MUST match
|
||||
# the metadata.json "dependencies" entry. For local development against an
|
||||
# un-pushed branch, override with a path:
|
||||
# lez_indexer_module.url = "path:../logos-execution-zone-indexer-module";
|
||||
# TODO: use the correct branch & repo name afterwards
|
||||
lez_indexer_module.url = "github:logos-blockchain/logos-execution-zone-indexer-module/erhant/migr-to-logos-module-builder";
|
||||
};
|
||||
|
||||
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
|
||||
pkgs.qt6.qtsvg
|
||||
pkgs.qt6.qtwebsockets
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
echo "LEZ Explorer UI development environment"
|
||||
'';
|
||||
};
|
||||
});
|
||||
outputs =
|
||||
inputs@{ logos-module-builder, ... }:
|
||||
logos-module-builder.lib.mkLogosModule {
|
||||
src = ./.;
|
||||
configFile = ./metadata.json;
|
||||
flakeInputs = inputs;
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,10 +1,22 @@
|
||||
{
|
||||
"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"]
|
||||
"description": "LEZ Explorer UI - A block explorer for the Logos Execution Zone",
|
||||
"main": "lez_explorer_ui_plugin",
|
||||
"dependencies": ["lez_indexer_module"],
|
||||
"nix": {
|
||||
"packages": {
|
||||
"build": ["qt6.qtsvg"],
|
||||
"runtime": ["qt6.qtsvg"]
|
||||
},
|
||||
"external_libraries": [],
|
||||
"cmake": {
|
||||
"find_packages": ["Qt6Widgets", "Qt6Svg", "Qt6SvgWidgets", "Qt6Network"],
|
||||
"extra_sources": [],
|
||||
"extra_include_dirs": ["interfaces"],
|
||||
"extra_link_libraries": ["Qt6::Widgets", "Qt6::Svg", "Qt6::SvgWidgets", "Qt6::Network"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
65
nix/app.nix
65
nix/app.nix
@ -1,65 +0,0 @@
|
||||
# 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.qt6.qtwebsockets
|
||||
] ++ 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
|
||||
'';
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
# 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
|
||||
pkgs.qt6.qtwebsockets
|
||||
];
|
||||
|
||||
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
45
nix/lib.nix
@ -1,45 +0,0 @@
|
||||
# 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
|
||||
'';
|
||||
}
|
||||
@ -6,9 +6,9 @@ ExplorerPlugin::ExplorerPlugin(QObject* parent)
|
||||
{
|
||||
}
|
||||
|
||||
QWidget* ExplorerPlugin::createWidget(LogosAPI* /*logosAPI*/)
|
||||
QWidget* ExplorerPlugin::createWidget(LogosAPI* logosAPI)
|
||||
{
|
||||
return new ExplorerWidget();
|
||||
return new ExplorerWidget(logosAPI);
|
||||
}
|
||||
|
||||
void ExplorerPlugin::destroyWidget(QWidget* widget)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#include "ExplorerWidget.h"
|
||||
#include "Style.h"
|
||||
#include "services/RpcIndexerService.h"
|
||||
#include "services/LpIndexerService.h"
|
||||
#include "widgets/NavigationBar.h"
|
||||
#include "widgets/SearchBar.h"
|
||||
#include "pages/MainPage.h"
|
||||
@ -12,9 +12,9 @@
|
||||
#include <QStackedWidget>
|
||||
#include <QLabel>
|
||||
|
||||
ExplorerWidget::ExplorerWidget(QWidget* parent)
|
||||
ExplorerWidget::ExplorerWidget(LogosAPI* logosAPI, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_indexer(std::make_unique<RpcIndexerService>())
|
||||
, m_indexer(std::make_unique<LpIndexerService>(logosAPI))
|
||||
{
|
||||
setStyleSheet(Style::appBackground());
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ class QStackedWidget;
|
||||
class NavigationBar;
|
||||
class SearchBar;
|
||||
class MainPage;
|
||||
class LogosAPI;
|
||||
|
||||
struct NavHome {};
|
||||
struct NavBlock { quint64 blockId; };
|
||||
@ -23,7 +24,7 @@ class ExplorerWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ExplorerWidget(QWidget* parent = nullptr);
|
||||
explicit ExplorerWidget(LogosAPI* logosAPI = nullptr, QWidget* parent = nullptr);
|
||||
~ExplorerWidget() override;
|
||||
|
||||
private:
|
||||
|
||||
@ -30,5 +30,4 @@ struct Block {
|
||||
QString signature;
|
||||
QVector<Transaction> transactions;
|
||||
BedrockStatus bedrockStatus = BedrockStatus::Pending;
|
||||
QString bedrockParentId;
|
||||
};
|
||||
|
||||
@ -24,12 +24,12 @@ MainPage::MainPage(IndexerService* indexer, QWidget* parent)
|
||||
auto* endpointRow = new QHBoxLayout();
|
||||
endpointRow->setSpacing(8);
|
||||
|
||||
auto* endpointLabel = new QLabel("Indexer RPC", this);
|
||||
auto* endpointLabel = new QLabel("Indexer config", this);
|
||||
endpointLabel->setStyleSheet(Style::mutedText() + " font-weight: bold;");
|
||||
|
||||
m_indexerEndpointInput = new QLineEdit(this);
|
||||
m_indexerEndpointInput->setText(m_indexer->endpoint());
|
||||
m_indexerEndpointInput->setPlaceholderText(IndexerService::defaultEndpoint());
|
||||
m_indexerEndpointInput->setPlaceholderText("absolute path to indexer_config.json (blank = already running)");
|
||||
m_indexerEndpointInput->setMinimumHeight(34);
|
||||
m_indexerEndpointInput->setStyleSheet(Style::searchInput());
|
||||
|
||||
@ -358,13 +358,10 @@ void MainPage::loadMoreBlocks()
|
||||
|
||||
void MainPage::applyIndexerEndpoint()
|
||||
{
|
||||
QString endpoint = m_indexerEndpointInput->text().trimmed();
|
||||
if (endpoint.isEmpty()) {
|
||||
endpoint = IndexerService::defaultEndpoint();
|
||||
m_indexerEndpointInput->setText(endpoint);
|
||||
}
|
||||
|
||||
m_indexer->setEndpoint(endpoint);
|
||||
// The "endpoint" is the indexer config path now; blank is valid and means
|
||||
// "the indexer is already running" (start is idempotent provider-side).
|
||||
const QString configPath = m_indexerEndpointInput->text().trimmed();
|
||||
m_indexer->setEndpoint(configPath);
|
||||
clearSearchResults();
|
||||
refresh();
|
||||
}
|
||||
|
||||
@ -21,9 +21,12 @@ public:
|
||||
explicit IndexerService(QObject* parent = nullptr) : QObject(parent) {}
|
||||
~IndexerService() override = default;
|
||||
|
||||
// Over the Logos protocol the "endpoint" is repurposed to the indexer
|
||||
// config path (absolute path to indexer_config.json); blank means the
|
||||
// indexer is already running. No network URL is involved anymore.
|
||||
static QString defaultEndpoint()
|
||||
{
|
||||
return "ws://localhost:8779";
|
||||
return QString();
|
||||
}
|
||||
|
||||
virtual QString endpoint() const = 0;
|
||||
|
||||
438
src/services/LpIndexerService.cpp
Normal file
438
src/services/LpIndexerService.cpp
Normal file
@ -0,0 +1,438 @@
|
||||
#include "LpIndexerService.h"
|
||||
|
||||
#include "logos_api.h"
|
||||
#include "logos_api_client.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QDateTime>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QRegularExpression>
|
||||
#include <QTimer>
|
||||
#include <QTimeZone>
|
||||
#include <QVariant>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto kModuleName = "lez_indexer_module";
|
||||
constexpr int kPollIntervalMs = 2000;
|
||||
|
||||
// 64/128-bit values arrive as decimal strings (to dodge JSON double precision
|
||||
// loss), but be lenient and also accept a JSON number.
|
||||
quint64 jsonU64(const QJsonValue& value)
|
||||
{
|
||||
if (value.isString()) {
|
||||
bool ok = false;
|
||||
const qulonglong parsed = value.toString().toULongLong(&ok);
|
||||
return ok ? parsed : 0;
|
||||
}
|
||||
if (value.isDouble()) {
|
||||
const double number = value.toDouble();
|
||||
return number > 0.0 ? static_cast<quint64>(number) : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
QString jsonStr(const QJsonValue& value)
|
||||
{
|
||||
if (value.isString()) {
|
||||
return value.toString();
|
||||
}
|
||||
if (value.isDouble()) {
|
||||
return QString::number(static_cast<qulonglong>(value.toDouble()));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
QDateTime timestampFromSeconds(quint64 timestamp)
|
||||
{
|
||||
QDateTime dt;
|
||||
// Tolerate either seconds or milliseconds since epoch.
|
||||
if (timestamp > 1000000000000ULL) {
|
||||
dt = QDateTime::fromMSecsSinceEpoch(static_cast<qint64>(timestamp), QTimeZone::UTC);
|
||||
} else {
|
||||
dt = QDateTime::fromSecsSinceEpoch(static_cast<qint64>(timestamp), QTimeZone::UTC);
|
||||
}
|
||||
return dt.isValid() ? dt : QDateTime::currentDateTimeUtc();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LpIndexerService::LpIndexerService(LogosAPI* logosAPI, const QString& configPath, const QString& port, QObject* parent)
|
||||
: IndexerService(parent)
|
||||
, m_logosAPI(logosAPI)
|
||||
, m_configPath(configPath.trimmed())
|
||||
, m_port(port.trimmed())
|
||||
{
|
||||
if (m_logosAPI) {
|
||||
m_client = m_logosAPI->getClient(kModuleName);
|
||||
}
|
||||
if (!m_client) {
|
||||
qWarning() << "LpIndexerService: no LogosAPI client for" << kModuleName;
|
||||
}
|
||||
|
||||
// Best-effort boot (no-op provider-side if already running, or if no config).
|
||||
startIndexer();
|
||||
|
||||
// The provider has no push event — poll the tip for live head updates.
|
||||
m_pollTimer = new QTimer(this);
|
||||
m_pollTimer->setInterval(kPollIntervalMs);
|
||||
connect(m_pollTimer, &QTimer::timeout, this, &LpIndexerService::pollForNewBlocks);
|
||||
m_pollTimer->start();
|
||||
}
|
||||
|
||||
LpIndexerService::~LpIndexerService() = default;
|
||||
|
||||
QString LpIndexerService::endpoint() const
|
||||
{
|
||||
return m_configPath;
|
||||
}
|
||||
|
||||
void LpIndexerService::setEndpoint(const QString& endpoint)
|
||||
{
|
||||
const QString trimmed = endpoint.trimmed();
|
||||
if (trimmed == m_configPath) {
|
||||
return;
|
||||
}
|
||||
m_configPath = trimmed;
|
||||
// Re-baseline the live poller; the (re)started indexer may ingest afresh.
|
||||
m_lastNotifiedBlockId = 0;
|
||||
m_polledOnce = false;
|
||||
startIndexer();
|
||||
}
|
||||
|
||||
void LpIndexerService::startIndexer()
|
||||
{
|
||||
if (!m_client || m_configPath.isEmpty()) {
|
||||
return; // assume the indexer was started elsewhere
|
||||
}
|
||||
const QVariant result = m_client->invokeRemoteMethod(kModuleName, "start_indexer", m_configPath, m_port);
|
||||
const int code = result.isValid() ? result.toInt() : -1;
|
||||
if (code != 0) {
|
||||
qWarning() << "LpIndexerService: start_indexer returned" << code << "for" << m_configPath;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<Account> LpIndexerService::getAccount(const QString& accountId)
|
||||
{
|
||||
if (!m_client) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const QVariant r = m_client->invokeRemoteMethod(kModuleName, "getAccount", accountId);
|
||||
return parseAccount(accountId, r.isValid() ? r.toString() : QString());
|
||||
}
|
||||
|
||||
std::optional<Block> LpIndexerService::getBlockById(quint64 blockId)
|
||||
{
|
||||
if (!m_client) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const QVariant r = m_client->invokeRemoteMethod(kModuleName, "getBlockById", QString::number(blockId));
|
||||
return parseBlock(r.isValid() ? r.toString() : QString());
|
||||
}
|
||||
|
||||
std::optional<Block> LpIndexerService::getBlockByHash(const QString& hash)
|
||||
{
|
||||
if (!m_client) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const QVariant r = m_client->invokeRemoteMethod(kModuleName, "getBlockByHash", hash);
|
||||
return parseBlock(r.isValid() ? r.toString() : QString());
|
||||
}
|
||||
|
||||
std::optional<Transaction> LpIndexerService::getTransaction(const QString& hash)
|
||||
{
|
||||
if (!m_client) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const QVariant r = m_client->invokeRemoteMethod(kModuleName, "getTransaction", hash);
|
||||
const QString json = r.isValid() ? r.toString() : QString();
|
||||
if (json.isEmpty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(json.toUtf8());
|
||||
if (!doc.isObject()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return parseTransaction(doc.object());
|
||||
}
|
||||
|
||||
QVector<Block> LpIndexerService::getBlocks(std::optional<quint64> before, int limit)
|
||||
{
|
||||
if (!m_client) {
|
||||
return {};
|
||||
}
|
||||
const QString beforeArg = before.has_value() ? QString::number(*before) : QString();
|
||||
const QVariant r = m_client->invokeRemoteMethod(kModuleName, "getBlocks", beforeArg, QString::number(limit));
|
||||
const QString json = r.isValid() ? r.toString() : QString();
|
||||
if (json.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(json.toUtf8());
|
||||
if (!doc.isArray()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QVector<Block> blocks;
|
||||
const QJsonArray array = doc.array();
|
||||
blocks.reserve(array.size());
|
||||
for (const auto& item : array) {
|
||||
if (auto block = parseBlockObject(item.toObject())) {
|
||||
blocks.append(*block);
|
||||
}
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
quint64 LpIndexerService::getLastFinalizedBlockId()
|
||||
{
|
||||
if (!m_client) {
|
||||
return 0;
|
||||
}
|
||||
const QVariant r = m_client->invokeRemoteMethod(kModuleName, "getLastFinalizedBlockId");
|
||||
if (!r.isValid()) {
|
||||
return 0;
|
||||
}
|
||||
bool ok = false;
|
||||
const qulonglong parsed = r.toString().toULongLong(&ok);
|
||||
return ok ? parsed : 0;
|
||||
}
|
||||
|
||||
QVector<Transaction> LpIndexerService::getTransactionsByAccount(const QString& accountId, int offset, int limit)
|
||||
{
|
||||
if (!m_client) {
|
||||
return {};
|
||||
}
|
||||
const QVariant r = m_client->invokeRemoteMethod(
|
||||
kModuleName, "getTransactionsByAccount", accountId, QString::number(offset), QString::number(limit));
|
||||
const QString json = r.isValid() ? r.toString() : QString();
|
||||
if (json.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(json.toUtf8());
|
||||
if (!doc.isArray()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QVector<Transaction> transactions;
|
||||
const QJsonArray array = doc.array();
|
||||
transactions.reserve(array.size());
|
||||
for (const auto& item : array) {
|
||||
if (auto tx = parseTransaction(item.toObject())) {
|
||||
transactions.append(*tx);
|
||||
}
|
||||
}
|
||||
return transactions;
|
||||
}
|
||||
|
||||
SearchResults LpIndexerService::search(const QString& query)
|
||||
{
|
||||
SearchResults results;
|
||||
const QString trimmed = query.trimmed();
|
||||
if (trimmed.isEmpty()) {
|
||||
return results;
|
||||
}
|
||||
|
||||
static const QRegularExpression hashRegex(QStringLiteral("^[0-9a-fA-F]{64}$"));
|
||||
if (hashRegex.match(trimmed).hasMatch()) {
|
||||
if (auto block = getBlockByHash(trimmed)) {
|
||||
results.blocks.append(*block);
|
||||
}
|
||||
if (auto tx = getTransaction(trimmed)) {
|
||||
results.transactions.append(*tx);
|
||||
}
|
||||
if (auto account = getAccount(trimmed)) {
|
||||
results.accounts.append(*account);
|
||||
}
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
const quint64 blockId = trimmed.toULongLong(&ok);
|
||||
if (ok) {
|
||||
if (auto block = getBlockById(blockId)) {
|
||||
const bool duplicate = std::any_of(results.blocks.begin(), results.blocks.end(),
|
||||
[blockId](const Block& existing) { return existing.blockId == blockId; });
|
||||
if (!duplicate) {
|
||||
results.blocks.append(*block);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
void LpIndexerService::pollForNewBlocks()
|
||||
{
|
||||
const quint64 latest = getLastFinalizedBlockId();
|
||||
if (latest == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// First successful poll establishes a baseline without re-emitting blocks
|
||||
// the initial MainPage::refresh already loaded.
|
||||
if (!m_polledOnce) {
|
||||
m_polledOnce = true;
|
||||
m_lastNotifiedBlockId = latest;
|
||||
return;
|
||||
}
|
||||
|
||||
if (latest <= m_lastNotifiedBlockId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For a large jump (or a gap), emit only the new head: MainPage::onNewBlock
|
||||
// detects the discontinuity and rebuilds the recent list from the backend.
|
||||
if (latest - m_lastNotifiedBlockId > 8) {
|
||||
if (auto block = getBlockById(latest)) {
|
||||
m_lastNotifiedBlockId = latest;
|
||||
emit newBlockAdded(*block);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (quint64 id = m_lastNotifiedBlockId + 1; id <= latest; ++id) {
|
||||
if (auto block = getBlockById(id)) {
|
||||
emit newBlockAdded(*block);
|
||||
}
|
||||
}
|
||||
m_lastNotifiedBlockId = latest;
|
||||
}
|
||||
|
||||
std::optional<Block> LpIndexerService::parseBlock(const QString& json) const
|
||||
{
|
||||
if (json.isEmpty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(json.toUtf8());
|
||||
if (!doc.isObject()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return parseBlockObject(doc.object());
|
||||
}
|
||||
|
||||
std::optional<Block> LpIndexerService::parseBlockObject(const QJsonObject& obj) const
|
||||
{
|
||||
if (obj.isEmpty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Block block;
|
||||
block.blockId = jsonU64(obj.value("block_id"));
|
||||
block.hash = jsonStr(obj.value("hash"));
|
||||
block.prevBlockHash = jsonStr(obj.value("prev_block_hash"));
|
||||
block.signature = jsonStr(obj.value("signature"));
|
||||
block.timestamp = timestampFromSeconds(jsonU64(obj.value("timestamp")));
|
||||
|
||||
const QString status = obj.value("bedrock_status").toString("Pending");
|
||||
if (status == "Finalized") {
|
||||
block.bedrockStatus = BedrockStatus::Finalized;
|
||||
} else if (status == "Safe") {
|
||||
block.bedrockStatus = BedrockStatus::Safe;
|
||||
} else {
|
||||
block.bedrockStatus = BedrockStatus::Pending;
|
||||
}
|
||||
|
||||
const QJsonArray txArray = obj.value("transactions").toArray();
|
||||
block.transactions.reserve(txArray.size());
|
||||
for (const auto& txValue : txArray) {
|
||||
if (auto tx = parseTransaction(txValue.toObject())) {
|
||||
block.transactions.append(*tx);
|
||||
}
|
||||
}
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
std::optional<Transaction> LpIndexerService::parseTransaction(const QJsonObject& obj) const
|
||||
{
|
||||
if (obj.isEmpty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Transaction tx;
|
||||
tx.hash = jsonStr(obj.value("hash"));
|
||||
|
||||
const QString type = obj.value("type").toString();
|
||||
|
||||
auto appendAccounts = [&tx](const QJsonArray& accounts) {
|
||||
for (const auto& entry : accounts) {
|
||||
const QJsonObject account = entry.toObject();
|
||||
AccountRef ref;
|
||||
ref.accountId = jsonStr(account.value("account_id"));
|
||||
ref.nonce = jsonStr(account.value("nonce"));
|
||||
if (ref.nonce.isEmpty()) {
|
||||
ref.nonce = "0";
|
||||
}
|
||||
tx.accounts.append(ref);
|
||||
}
|
||||
};
|
||||
|
||||
if (type == "Public") {
|
||||
tx.type = TransactionType::Public;
|
||||
tx.programId = jsonStr(obj.value("program_id"));
|
||||
appendAccounts(obj.value("accounts").toArray());
|
||||
|
||||
const QJsonArray instructionData = obj.value("instruction_data").toArray();
|
||||
tx.instructionData.reserve(instructionData.size());
|
||||
for (const auto& v : instructionData) {
|
||||
tx.instructionData.append(static_cast<quint32>(jsonU64(v)));
|
||||
}
|
||||
tx.signatureCount = obj.value("signature_count").toInt();
|
||||
return tx;
|
||||
}
|
||||
|
||||
if (type == "PrivacyPreserving") {
|
||||
tx.type = TransactionType::PrivacyPreserving;
|
||||
appendAccounts(obj.value("accounts").toArray());
|
||||
tx.newCommitmentsCount = obj.value("new_commitments_count").toInt();
|
||||
tx.nullifiersCount = obj.value("nullifiers_count").toInt();
|
||||
tx.encryptedStatesCount = obj.value("encrypted_states_count").toInt();
|
||||
tx.validityWindowStart = jsonU64(obj.value("validity_window_start"));
|
||||
tx.validityWindowEnd = jsonU64(obj.value("validity_window_end"));
|
||||
tx.signatureCount = obj.value("signature_count").toInt();
|
||||
tx.proofSizeBytes = obj.value("proof_size").toInt();
|
||||
return tx;
|
||||
}
|
||||
|
||||
if (type == "ProgramDeployment") {
|
||||
tx.type = TransactionType::ProgramDeployment;
|
||||
tx.bytecodeSizeBytes = obj.value("bytecode_size").toInt();
|
||||
return tx;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<Account> LpIndexerService::parseAccount(const QString& accountId, const QString& json) const
|
||||
{
|
||||
if (json.isEmpty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(json.toUtf8());
|
||||
if (!doc.isObject()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const QJsonObject obj = doc.object();
|
||||
|
||||
Account account;
|
||||
account.accountId = accountId; // provider payload omits it; inject the queried id
|
||||
account.programOwner = jsonStr(obj.value("program_owner"));
|
||||
account.balance = jsonStr(obj.value("balance"));
|
||||
account.nonce = jsonStr(obj.value("nonce"));
|
||||
account.dataSizeBytes = obj.value("data_size").toInt();
|
||||
|
||||
if (account.balance.isEmpty()) {
|
||||
account.balance = "0";
|
||||
}
|
||||
if (account.nonce.isEmpty()) {
|
||||
account.nonce = "0";
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
69
src/services/LpIndexerService.h
Normal file
69
src/services/LpIndexerService.h
Normal file
@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include "IndexerService.h"
|
||||
|
||||
#include <QString>
|
||||
#include <optional>
|
||||
|
||||
class LogosAPI;
|
||||
class LogosAPIClient;
|
||||
class QTimer;
|
||||
class QJsonObject;
|
||||
|
||||
// IndexerService implementation that talks to `lez_indexer_module` IN-PROCESS
|
||||
// over the Logos protocol (Qt Remote Objects), via
|
||||
// LogosAPI::getClient(...)->invokeRemoteMethod(...). Replaces the old WebSocket
|
||||
// JSON-RPC RpcIndexerService.
|
||||
//
|
||||
// The provider returns a flat, compact JSON schema (hex hashes; 64/128-bit
|
||||
// values as decimal strings; counts/sizes as numbers; "" == not-found), which
|
||||
// differs from the old RPC schema — so this class carries its OWN parsers.
|
||||
//
|
||||
// The provider exposes no new-block push event, so live head updates are POLLED
|
||||
// on a 2 s timer over getLastFinalizedBlockId.
|
||||
//
|
||||
// `endpoint()`/`setEndpoint()` are repurposed to the indexer CONFIG PATH (an
|
||||
// absolute path to indexer_config.json) used to best-effort start the indexer;
|
||||
// blank means "the indexer is already running" (start is idempotent provider-side).
|
||||
class LpIndexerService : public IndexerService {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit LpIndexerService(
|
||||
LogosAPI* logosAPI,
|
||||
const QString& configPath = QString(),
|
||||
const QString& port = QStringLiteral("8779"),
|
||||
QObject* parent = nullptr
|
||||
);
|
||||
~LpIndexerService() override;
|
||||
|
||||
QString endpoint() const override; // the indexer config path
|
||||
void setEndpoint(const QString& endpoint) override; // set config path + (re)start
|
||||
|
||||
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 getLastFinalizedBlockId() override;
|
||||
QVector<Transaction> getTransactionsByAccount(const QString& accountId, int offset, int limit) override;
|
||||
SearchResults search(const QString& query) override;
|
||||
|
||||
private:
|
||||
void startIndexer();
|
||||
void pollForNewBlocks();
|
||||
|
||||
// Parsers for the provider's compact schema.
|
||||
std::optional<Block> parseBlock(const QString& json) const;
|
||||
std::optional<Block> parseBlockObject(const QJsonObject& obj) const;
|
||||
std::optional<Transaction> parseTransaction(const QJsonObject& obj) const;
|
||||
std::optional<Account> parseAccount(const QString& accountId, const QString& json) const;
|
||||
|
||||
LogosAPI* m_logosAPI = nullptr;
|
||||
LogosAPIClient* m_client = nullptr;
|
||||
QString m_configPath;
|
||||
QString m_port;
|
||||
QTimer* m_pollTimer = nullptr;
|
||||
quint64 m_lastNotifiedBlockId = 0;
|
||||
bool m_polledOnce = false;
|
||||
};
|
||||
@ -63,7 +63,6 @@ Block MockIndexerService::generateBlock(quint64 blockId, const QString& 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);
|
||||
@ -218,7 +217,6 @@ void MockIndexerService::generateData()
|
||||
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) {
|
||||
|
||||
@ -1,741 +0,0 @@
|
||||
#include "RpcIndexerService.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QDateTime>
|
||||
#include <QEventLoop>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonValue>
|
||||
#include <QRegularExpression>
|
||||
#include <QTimer>
|
||||
#include <QTimeZone>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace {
|
||||
|
||||
QString compactJson(const QJsonValue& value)
|
||||
{
|
||||
if (value.isObject()) {
|
||||
return QString::fromUtf8(QJsonDocument(value.toObject()).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
if (value.isArray()) {
|
||||
return QString::fromUtf8(QJsonDocument(value.toArray()).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
quint64 extractFinalizedBlockId(const QJsonValue& value)
|
||||
{
|
||||
if (value.isDouble()) {
|
||||
const double number = value.toDouble();
|
||||
if (number <= 0.0) {
|
||||
return 0;
|
||||
}
|
||||
return static_cast<quint64>(number);
|
||||
}
|
||||
|
||||
if (value.isString()) {
|
||||
bool ok = false;
|
||||
const qulonglong parsed = value.toString().toULongLong(&ok);
|
||||
return ok ? parsed : 0;
|
||||
}
|
||||
|
||||
if (value.isObject()) {
|
||||
const QJsonObject obj = value.toObject();
|
||||
if (obj.contains("block_id")) {
|
||||
return extractFinalizedBlockId(obj.value("block_id"));
|
||||
}
|
||||
if (obj.contains("blockId")) {
|
||||
return extractFinalizedBlockId(obj.value("blockId"));
|
||||
}
|
||||
if (obj.contains("id")) {
|
||||
return extractFinalizedBlockId(obj.value("id"));
|
||||
}
|
||||
if (obj.contains("result")) {
|
||||
return extractFinalizedBlockId(obj.value("result"));
|
||||
}
|
||||
if (obj.contains("header")) {
|
||||
return extractFinalizedBlockId(obj.value("header").toObject().value("block_id"));
|
||||
}
|
||||
}
|
||||
|
||||
if (value.isArray()) {
|
||||
const QJsonArray arr = value.toArray();
|
||||
for (const auto& item : arr) {
|
||||
const quint64 blockId = extractFinalizedBlockId(item);
|
||||
if (blockId) {
|
||||
return blockId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
RpcIndexerService::RpcIndexerService(const QString& endpoint, QObject* parent)
|
||||
: IndexerService(parent)
|
||||
, m_endpoint(QUrl(endpoint.trimmed()))
|
||||
{
|
||||
connect(&m_socket, &QWebSocket::connected, this, &RpcIndexerService::onConnected);
|
||||
connect(&m_socket, &QWebSocket::disconnected, this, &RpcIndexerService::onDisconnected);
|
||||
connect(&m_socket, &QWebSocket::textMessageReceived, this, &RpcIndexerService::onTextMessageReceived);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||
connect(&m_socket, &QWebSocket::errorOccurred, this, &RpcIndexerService::onSocketError);
|
||||
#else
|
||||
connect(&m_socket,
|
||||
QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error),
|
||||
this,
|
||||
&RpcIndexerService::onSocketError);
|
||||
#endif
|
||||
}
|
||||
|
||||
RpcIndexerService::~RpcIndexerService()
|
||||
{
|
||||
m_socket.close();
|
||||
}
|
||||
|
||||
QString RpcIndexerService::endpoint() const
|
||||
{
|
||||
return m_endpoint.toString();
|
||||
}
|
||||
|
||||
void RpcIndexerService::setEndpoint(const QString& endpoint)
|
||||
{
|
||||
const QString trimmed = endpoint.trimmed();
|
||||
const QUrl newEndpoint(trimmed.isEmpty() ? IndexerService::defaultEndpoint() : trimmed);
|
||||
if (!newEndpoint.isValid()) {
|
||||
qWarning() << "Invalid indexer endpoint URL:" << endpoint;
|
||||
return;
|
||||
}
|
||||
|
||||
if (newEndpoint == m_endpoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_endpoint = newEndpoint;
|
||||
m_nextRequestId = 1;
|
||||
m_responses.clear();
|
||||
m_subscriptionRequestId = 0;
|
||||
m_subscriptionId = QJsonValue();
|
||||
m_subscriptionActive = false;
|
||||
m_lastNotifiedBlockId = 0;
|
||||
|
||||
for (auto it = m_waiters.begin(); it != m_waiters.end(); ++it) {
|
||||
if (it.value()) {
|
||||
it.value()->quit();
|
||||
}
|
||||
}
|
||||
m_waiters.clear();
|
||||
|
||||
if (m_socket.state() != QAbstractSocket::UnconnectedState) {
|
||||
m_socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<Account> RpcIndexerService::getAccount(const QString& accountId)
|
||||
{
|
||||
auto result = callRpc("getAccount", QJsonArray{accountId});
|
||||
if (!result) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return parseAccount(accountId, *result);
|
||||
}
|
||||
|
||||
std::optional<Block> RpcIndexerService::getBlockById(quint64 blockId)
|
||||
{
|
||||
auto result = callRpc("getBlockById", QJsonArray{static_cast<qint64>(blockId)});
|
||||
if (!result || result->isNull()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return parseBlock(*result);
|
||||
}
|
||||
|
||||
std::optional<Block> RpcIndexerService::getBlockByHash(const QString& hash)
|
||||
{
|
||||
auto result = callRpc("getBlockByHash", QJsonArray{hash});
|
||||
if (!result || result->isNull()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return parseBlock(*result);
|
||||
}
|
||||
|
||||
std::optional<Transaction> RpcIndexerService::getTransaction(const QString& hash)
|
||||
{
|
||||
auto result = callRpc("getTransaction", QJsonArray{hash});
|
||||
if (!result || result->isNull()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return parseTransaction(*result);
|
||||
}
|
||||
|
||||
QVector<Block> RpcIndexerService::getBlocks(std::optional<quint64> before, int limit)
|
||||
{
|
||||
QJsonArray params;
|
||||
if (before.has_value()) {
|
||||
params.append(static_cast<qint64>(*before));
|
||||
} else {
|
||||
params.append(QJsonValue::Null);
|
||||
}
|
||||
params.append(limit);
|
||||
|
||||
auto result = callRpc("getBlocks", params);
|
||||
if (!result || !result->isArray()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QVector<Block> blocks;
|
||||
const QJsonArray array = result->toArray();
|
||||
blocks.reserve(array.size());
|
||||
for (const auto& item : array) {
|
||||
auto block = parseBlock(item);
|
||||
if (block) {
|
||||
blocks.append(*block);
|
||||
}
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
quint64 RpcIndexerService::getLastFinalizedBlockId()
|
||||
{
|
||||
auto result = callRpc("getLastFinalizedBlockId");
|
||||
if (!result) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const quint64 latestFinalized = jsonValueToU64(*result);
|
||||
if (latestFinalized > m_lastNotifiedBlockId) {
|
||||
m_lastNotifiedBlockId = latestFinalized;
|
||||
}
|
||||
|
||||
return latestFinalized;
|
||||
}
|
||||
|
||||
QVector<Transaction> RpcIndexerService::getTransactionsByAccount(const QString& accountId, int offset, int limit)
|
||||
{
|
||||
auto result = callRpc("getTransactionsByAccount", QJsonArray{accountId, offset, limit});
|
||||
if (!result || !result->isArray()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QVector<Transaction> transactions;
|
||||
const QJsonArray array = result->toArray();
|
||||
transactions.reserve(array.size());
|
||||
for (const auto& item : array) {
|
||||
auto tx = parseTransaction(item);
|
||||
if (tx) {
|
||||
transactions.append(*tx);
|
||||
}
|
||||
}
|
||||
return transactions;
|
||||
}
|
||||
|
||||
SearchResults RpcIndexerService::search(const QString& query)
|
||||
{
|
||||
SearchResults results;
|
||||
const QString trimmed = query.trimmed();
|
||||
if (trimmed.isEmpty()) {
|
||||
return results;
|
||||
}
|
||||
|
||||
static const QRegularExpression hashRegex(QStringLiteral("^[0-9a-fA-F]{64}$"));
|
||||
if (hashRegex.match(trimmed).hasMatch()) {
|
||||
if (auto block = getBlockByHash(trimmed)) {
|
||||
results.blocks.append(*block);
|
||||
}
|
||||
if (auto tx = getTransaction(trimmed)) {
|
||||
results.transactions.append(*tx);
|
||||
}
|
||||
}
|
||||
|
||||
if (auto account = getAccount(trimmed)) {
|
||||
results.accounts.append(*account);
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
const quint64 blockId = trimmed.toULongLong(&ok);
|
||||
if (ok) {
|
||||
if (auto block = getBlockById(blockId)) {
|
||||
const auto duplicate = std::any_of(results.blocks.begin(), results.blocks.end(),
|
||||
[blockId](const Block& existing) {
|
||||
return existing.blockId == blockId;
|
||||
});
|
||||
if (!duplicate) {
|
||||
results.blocks.append(*block);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
void RpcIndexerService::onConnected()
|
||||
{
|
||||
qInfo() << "Connected to indexer RPC:" << m_endpoint;
|
||||
ensureFinalizedBlocksSubscription();
|
||||
}
|
||||
|
||||
void RpcIndexerService::onDisconnected()
|
||||
{
|
||||
m_subscriptionRequestId = 0;
|
||||
m_subscriptionId = QJsonValue();
|
||||
m_subscriptionActive = false;
|
||||
|
||||
for (auto it = m_waiters.begin(); it != m_waiters.end(); ++it) {
|
||||
if (it.value()) {
|
||||
it.value()->quit();
|
||||
}
|
||||
}
|
||||
m_waiters.clear();
|
||||
}
|
||||
|
||||
void RpcIndexerService::onSocketError(QAbstractSocket::SocketError error)
|
||||
{
|
||||
Q_UNUSED(error);
|
||||
qWarning() << "Indexer RPC socket error:" << m_socket.errorString();
|
||||
}
|
||||
|
||||
void RpcIndexerService::onTextMessageReceived(const QString& message)
|
||||
{
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8(), &parseError);
|
||||
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
qWarning() << "Invalid JSON-RPC payload:" << parseError.errorString();
|
||||
return;
|
||||
}
|
||||
|
||||
const QJsonObject payload = doc.object();
|
||||
|
||||
const QString method = payload.value("method").toString();
|
||||
if (method == "subscribeToFinalizedBlocks") {
|
||||
const QJsonObject params = payload.value("params").toObject();
|
||||
if (params.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_subscriptionId.isNull() && params.contains("subscription")) {
|
||||
const QJsonValue subscriptionValue = params.value("subscription");
|
||||
if (subscriptionValue != m_subscriptionId) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
handleFinalizedBlockNotification(params.value("result"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!payload.contains("id")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const quint64 id = jsonValueToU64(payload.value("id"));
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (id == m_subscriptionRequestId) {
|
||||
m_subscriptionRequestId = 0;
|
||||
if (payload.contains("error")) {
|
||||
m_subscriptionActive = false;
|
||||
m_subscriptionId = QJsonValue();
|
||||
qWarning() << "Failed to subscribe to finalized blocks:" << compactJson(payload.value("error"));
|
||||
} else {
|
||||
m_subscriptionId = payload.value("result");
|
||||
m_subscriptionActive = !m_subscriptionId.isUndefined() && !m_subscriptionId.isNull();
|
||||
if (!m_subscriptionActive) {
|
||||
qWarning() << "Unexpected finalized blocks subscription result";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_responses.insert(id, payload);
|
||||
auto* waiter = m_waiters.value(id, nullptr);
|
||||
if (waiter) {
|
||||
waiter->quit();
|
||||
}
|
||||
}
|
||||
|
||||
void RpcIndexerService::handleFinalizedBlockNotification(const QJsonValue& result)
|
||||
{
|
||||
const quint64 blockId = extractFinalizedBlockId(result);
|
||||
if (!blockId || blockId <= m_lastNotifiedBlockId) {
|
||||
return;
|
||||
}
|
||||
|
||||
QTimer::singleShot(0, this, [this, blockId]() {
|
||||
fetchAndEmitFinalizedBlock(blockId);
|
||||
});
|
||||
}
|
||||
|
||||
void RpcIndexerService::fetchAndEmitFinalizedBlock(quint64 blockId)
|
||||
{
|
||||
auto block = getBlockById(blockId);
|
||||
if (block) {
|
||||
if (blockId <= m_lastNotifiedBlockId) {
|
||||
return;
|
||||
}
|
||||
m_lastNotifiedBlockId = blockId;
|
||||
emit newBlockAdded(*block);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to latest block snapshot if the specific id is briefly unavailable.
|
||||
auto latestBlocks = getBlocks(std::nullopt, 1);
|
||||
if (!latestBlocks.isEmpty()) {
|
||||
const Block& latest = latestBlocks.first();
|
||||
if (latest.blockId >= blockId && latest.blockId > m_lastNotifiedBlockId) {
|
||||
m_lastNotifiedBlockId = latest.blockId;
|
||||
emit newBlockAdded(latest);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
qWarning() << "Finalized block announced but not yet available:" << blockId;
|
||||
}
|
||||
|
||||
bool RpcIndexerService::ensureConnected()
|
||||
{
|
||||
if (!m_endpoint.isValid() || m_endpoint.scheme().isEmpty()) {
|
||||
qWarning() << "Invalid indexer RPC URL:" << m_endpoint;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_socket.state() == QAbstractSocket::ConnectedState) {
|
||||
ensureFinalizedBlocksSubscription();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (m_socket.state() == QAbstractSocket::UnconnectedState) {
|
||||
m_socket.open(m_endpoint);
|
||||
}
|
||||
|
||||
QEventLoop waitLoop;
|
||||
QTimer timeout;
|
||||
bool timedOut = false;
|
||||
|
||||
timeout.setSingleShot(true);
|
||||
connect(&timeout, &QTimer::timeout, &waitLoop, [&]() {
|
||||
timedOut = true;
|
||||
waitLoop.quit();
|
||||
});
|
||||
|
||||
connect(&m_socket, &QWebSocket::connected, &waitLoop, &QEventLoop::quit);
|
||||
connect(&m_socket, &QWebSocket::disconnected, &waitLoop, &QEventLoop::quit);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||
connect(&m_socket, &QWebSocket::errorOccurred, &waitLoop, &QEventLoop::quit);
|
||||
#else
|
||||
connect(&m_socket,
|
||||
QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error),
|
||||
&waitLoop,
|
||||
&QEventLoop::quit);
|
||||
#endif
|
||||
|
||||
timeout.start(4000);
|
||||
waitLoop.exec();
|
||||
|
||||
if (timedOut) {
|
||||
qWarning() << "Timeout connecting to indexer RPC:" << m_endpoint;
|
||||
m_socket.abort();
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool isConnected = m_socket.state() == QAbstractSocket::ConnectedState;
|
||||
if (isConnected) {
|
||||
ensureFinalizedBlocksSubscription();
|
||||
}
|
||||
|
||||
return isConnected;
|
||||
}
|
||||
|
||||
void RpcIndexerService::ensureFinalizedBlocksSubscription()
|
||||
{
|
||||
if (m_socket.state() != QAbstractSocket::ConnectedState) {
|
||||
return;
|
||||
}
|
||||
if (m_subscriptionActive || m_subscriptionRequestId != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const quint64 requestId = m_nextRequestId++;
|
||||
m_subscriptionRequestId = requestId;
|
||||
|
||||
QJsonObject request;
|
||||
request.insert("jsonrpc", "2.0");
|
||||
request.insert("id", static_cast<qint64>(requestId));
|
||||
request.insert("method", "subscribeToFinalizedBlocks");
|
||||
request.insert("params", QJsonArray());
|
||||
|
||||
m_socket.sendTextMessage(QString::fromUtf8(QJsonDocument(request).toJson(QJsonDocument::Compact)));
|
||||
}
|
||||
|
||||
std::optional<QJsonValue> RpcIndexerService::callRpc(const QString& method, const QJsonArray& params)
|
||||
{
|
||||
if (!ensureConnected()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const quint64 id = m_nextRequestId++;
|
||||
QJsonObject request;
|
||||
request.insert("jsonrpc", "2.0");
|
||||
request.insert("id", static_cast<qint64>(id));
|
||||
request.insert("method", method);
|
||||
request.insert("params", params);
|
||||
|
||||
QEventLoop waitLoop;
|
||||
QTimer timeout;
|
||||
bool timedOut = false;
|
||||
|
||||
timeout.setSingleShot(true);
|
||||
connect(&timeout, &QTimer::timeout, &waitLoop, [&]() {
|
||||
timedOut = true;
|
||||
waitLoop.quit();
|
||||
});
|
||||
connect(&m_socket, &QWebSocket::disconnected, &waitLoop, &QEventLoop::quit);
|
||||
|
||||
m_waiters.insert(id, &waitLoop);
|
||||
m_socket.sendTextMessage(QString::fromUtf8(QJsonDocument(request).toJson(QJsonDocument::Compact)));
|
||||
|
||||
timeout.start(5000);
|
||||
waitLoop.exec();
|
||||
m_waiters.remove(id);
|
||||
|
||||
if (timedOut) {
|
||||
qWarning() << "RPC call timeout:" << method;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!m_responses.contains(id)) {
|
||||
qWarning() << "No RPC response for" << method;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QJsonObject response = m_responses.take(id);
|
||||
if (response.contains("error")) {
|
||||
qWarning() << "RPC error for" << method << compactJson(response.value("error"));
|
||||
return std::nullopt;
|
||||
}
|
||||
if (!response.contains("result")) {
|
||||
qWarning() << "RPC result missing for" << method;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const std::optional<QJsonValue> result = response.value("result");
|
||||
qDebug() << "RPC call result for" << method << "(" << params << "):" << jsonValueToString(*result);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<Block> RpcIndexerService::parseBlock(const QJsonValue& value) const
|
||||
{
|
||||
if (!value.isObject()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QJsonObject blockObj = value.toObject();
|
||||
const QJsonObject headerObj = blockObj.value("header").toObject();
|
||||
const QJsonObject bodyObj = blockObj.value("body").toObject();
|
||||
if (headerObj.isEmpty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Block block;
|
||||
block.blockId = jsonValueToU64(headerObj.value("block_id"));
|
||||
block.hash = jsonValueToString(headerObj.value("hash"));
|
||||
block.prevBlockHash = jsonValueToString(headerObj.value("prev_block_hash"));
|
||||
block.signature = jsonValueToString(headerObj.value("signature"));
|
||||
block.bedrockParentId = jsonValueToString(blockObj.value("bedrock_parent_id"));
|
||||
|
||||
const quint64 timestamp = jsonValueToU64(headerObj.value("timestamp"));
|
||||
if (timestamp > 1000000000000ULL) {
|
||||
block.timestamp = QDateTime::fromMSecsSinceEpoch(static_cast<qint64>(timestamp), QTimeZone::UTC);
|
||||
} else {
|
||||
block.timestamp = QDateTime::fromSecsSinceEpoch(static_cast<qint64>(timestamp), QTimeZone::UTC);
|
||||
}
|
||||
if (!block.timestamp.isValid()) {
|
||||
block.timestamp = QDateTime::currentDateTimeUtc();
|
||||
}
|
||||
|
||||
const QString status = blockObj.value("bedrock_status").toString("Pending");
|
||||
if (status == "Finalized") {
|
||||
block.bedrockStatus = BedrockStatus::Finalized;
|
||||
} else if (status == "Safe") {
|
||||
block.bedrockStatus = BedrockStatus::Safe;
|
||||
} else {
|
||||
block.bedrockStatus = BedrockStatus::Pending;
|
||||
}
|
||||
|
||||
const QJsonArray txArray = bodyObj.value("transactions").toArray();
|
||||
block.transactions.reserve(txArray.size());
|
||||
for (const auto& txValue : txArray) {
|
||||
auto tx = parseTransaction(txValue);
|
||||
if (tx) {
|
||||
block.transactions.append(*tx);
|
||||
}
|
||||
}
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
std::optional<Transaction> RpcIndexerService::parseTransaction(const QJsonValue& value) const
|
||||
{
|
||||
if (!value.isObject()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QJsonObject wrapper = value.toObject();
|
||||
Transaction tx;
|
||||
|
||||
auto appendAccounts = [&tx](const QJsonArray& accountIds, const QJsonArray& nonces) {
|
||||
const int pairedCount = std::min(accountIds.size(), nonces.size());
|
||||
for (int i = 0; i < pairedCount; ++i) {
|
||||
AccountRef account;
|
||||
account.accountId = jsonValueToString(accountIds.at(i));
|
||||
account.nonce = jsonValueToString(nonces.at(i));
|
||||
tx.accounts.append(account);
|
||||
}
|
||||
for (int i = pairedCount; i < accountIds.size(); ++i) {
|
||||
AccountRef account;
|
||||
account.accountId = jsonValueToString(accountIds.at(i));
|
||||
account.nonce = "0";
|
||||
tx.accounts.append(account);
|
||||
}
|
||||
};
|
||||
|
||||
auto fillWitnessFields = [&tx](const QJsonObject& witnessSet) {
|
||||
tx.signatureCount = witnessSet.value("signatures_and_public_keys").toArray().size();
|
||||
if (witnessSet.value("proof").isString()) {
|
||||
tx.proofSizeBytes = base64Size(witnessSet.value("proof").toString());
|
||||
}
|
||||
};
|
||||
|
||||
if (wrapper.contains("Public")) {
|
||||
const QJsonObject publicTx = wrapper.value("Public").toObject();
|
||||
const QJsonObject message = publicTx.value("message").toObject();
|
||||
|
||||
tx.type = TransactionType::Public;
|
||||
tx.hash = jsonValueToString(publicTx.value("hash"));
|
||||
tx.programId = jsonValueToString(message.value("program_id"));
|
||||
appendAccounts(message.value("account_ids").toArray(), message.value("nonces").toArray());
|
||||
|
||||
const QJsonArray instructionData = message.value("instruction_data").toArray();
|
||||
tx.instructionData.reserve(instructionData.size());
|
||||
for (const auto& v : instructionData) {
|
||||
tx.instructionData.append(static_cast<quint32>(jsonValueToU64(v)));
|
||||
}
|
||||
|
||||
fillWitnessFields(publicTx.value("witness_set").toObject());
|
||||
return tx;
|
||||
}
|
||||
|
||||
if (wrapper.contains("PrivacyPreserving")) {
|
||||
const QJsonObject privateTx = wrapper.value("PrivacyPreserving").toObject();
|
||||
const QJsonObject message = privateTx.value("message").toObject();
|
||||
|
||||
tx.type = TransactionType::PrivacyPreserving;
|
||||
tx.hash = jsonValueToString(privateTx.value("hash"));
|
||||
appendAccounts(message.value("public_account_ids").toArray(), message.value("nonces").toArray());
|
||||
|
||||
tx.newCommitmentsCount = message.value("new_commitments").toArray().size();
|
||||
tx.nullifiersCount = message.value("new_nullifiers").toArray().size();
|
||||
tx.encryptedStatesCount = message.value("encrypted_private_post_states").toArray().size();
|
||||
|
||||
const QJsonArray validityWindow = message.value("block_validity_window").toArray();
|
||||
if (!validityWindow.isEmpty()) {
|
||||
tx.validityWindowStart = jsonValueToU64(validityWindow.at(0));
|
||||
}
|
||||
if (validityWindow.size() > 1) {
|
||||
tx.validityWindowEnd = jsonValueToU64(validityWindow.at(1));
|
||||
}
|
||||
|
||||
fillWitnessFields(privateTx.value("witness_set").toObject());
|
||||
return tx;
|
||||
}
|
||||
|
||||
if (wrapper.contains("ProgramDeployment")) {
|
||||
const QJsonObject deployTx = wrapper.value("ProgramDeployment").toObject();
|
||||
const QJsonObject message = deployTx.value("message").toObject();
|
||||
|
||||
tx.type = TransactionType::ProgramDeployment;
|
||||
tx.hash = jsonValueToString(deployTx.value("hash"));
|
||||
tx.bytecodeSizeBytes = base64Size(message.value("bytecode").toString());
|
||||
tx.signatureCount = 0;
|
||||
tx.proofSizeBytes = 0;
|
||||
return tx;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<Account> RpcIndexerService::parseAccount(const QString& accountId, const QJsonValue& value) const
|
||||
{
|
||||
if (!value.isObject()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QJsonObject accountObj = value.toObject();
|
||||
|
||||
Account account;
|
||||
account.accountId = accountId;
|
||||
account.programOwner = jsonValueToString(accountObj.value("program_owner"));
|
||||
account.balance = jsonValueToString(accountObj.value("balance"));
|
||||
account.nonce = jsonValueToString(accountObj.value("nonce"));
|
||||
account.dataSizeBytes = base64Size(accountObj.value("data").toString());
|
||||
|
||||
if (account.balance.isEmpty()) {
|
||||
account.balance = "0";
|
||||
}
|
||||
if (account.nonce.isEmpty()) {
|
||||
account.nonce = "0";
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
QString RpcIndexerService::jsonValueToString(const QJsonValue& value)
|
||||
{
|
||||
if (value.isString()) {
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
if (value.isDouble()) {
|
||||
const double number = value.toDouble();
|
||||
const qulonglong integer = static_cast<qulonglong>(number);
|
||||
if (number == static_cast<double>(integer)) {
|
||||
return QString::number(integer);
|
||||
}
|
||||
return QString::number(number, 'g', 16);
|
||||
}
|
||||
|
||||
if (value.isBool()) {
|
||||
return value.toBool() ? "true" : "false";
|
||||
}
|
||||
|
||||
return compactJson(value);
|
||||
}
|
||||
|
||||
quint64 RpcIndexerService::jsonValueToU64(const QJsonValue& value)
|
||||
{
|
||||
if (value.isDouble()) {
|
||||
const double number = value.toDouble();
|
||||
if (number <= 0.0) {
|
||||
return 0;
|
||||
}
|
||||
return static_cast<quint64>(number);
|
||||
}
|
||||
|
||||
if (value.isString()) {
|
||||
bool ok = false;
|
||||
const qulonglong parsed = value.toString().toULongLong(&ok);
|
||||
return ok ? parsed : 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int RpcIndexerService::base64Size(const QString& value)
|
||||
{
|
||||
if (value.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
return QByteArray::fromBase64(value.toUtf8()).size();
|
||||
}
|
||||
@ -1,66 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "IndexerService.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QUrl>
|
||||
#include <QWebSocket>
|
||||
|
||||
#include <optional>
|
||||
|
||||
class QEventLoop;
|
||||
|
||||
class RpcIndexerService : public IndexerService {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit RpcIndexerService(const QString& endpoint = IndexerService::defaultEndpoint(), QObject* parent = nullptr);
|
||||
~RpcIndexerService() override;
|
||||
|
||||
QString endpoint() const override;
|
||||
void setEndpoint(const QString& endpoint) override;
|
||||
|
||||
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 getLastFinalizedBlockId() override;
|
||||
QVector<Transaction> getTransactionsByAccount(const QString& accountId, int offset, int limit) override;
|
||||
SearchResults search(const QString& query) override;
|
||||
|
||||
private slots:
|
||||
void onConnected();
|
||||
void onDisconnected();
|
||||
void onSocketError(QAbstractSocket::SocketError error);
|
||||
void onTextMessageReceived(const QString& message);
|
||||
|
||||
private:
|
||||
void handleFinalizedBlockNotification(const QJsonValue& result);
|
||||
void fetchAndEmitFinalizedBlock(quint64 blockId);
|
||||
|
||||
bool ensureConnected();
|
||||
void ensureFinalizedBlocksSubscription();
|
||||
std::optional<QJsonValue> callRpc(const QString& method, const QJsonArray& params = QJsonArray());
|
||||
|
||||
std::optional<Block> parseBlock(const QJsonValue& value) const;
|
||||
std::optional<Transaction> parseTransaction(const QJsonValue& value) const;
|
||||
std::optional<Account> parseAccount(const QString& accountId, const QJsonValue& value) const;
|
||||
|
||||
static QString jsonValueToString(const QJsonValue& value);
|
||||
static quint64 jsonValueToU64(const QJsonValue& value);
|
||||
static int base64Size(const QString& value);
|
||||
|
||||
QWebSocket m_socket;
|
||||
QUrl m_endpoint;
|
||||
quint64 m_nextRequestId = 1;
|
||||
QHash<quint64, QJsonObject> m_responses;
|
||||
QHash<quint64, QEventLoop*> m_waiters;
|
||||
quint64 m_subscriptionRequestId = 0;
|
||||
QJsonValue m_subscriptionId;
|
||||
bool m_subscriptionActive = false;
|
||||
quint64 m_lastNotifiedBlockId = 0;
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user