mirror of
https://github.com/logos-blockchain/lez-explorer-ui.git
synced 2026-05-29 21:39:29 +00:00
feat: add real connection
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
a99d883240
commit
34a37bcbe7
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
||||
result
|
||||
build
|
||||
|
||||
@ -8,7 +8,7 @@ set(CMAKE_AUTORCC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
|
||||
# Find Qt packages
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Widgets Svg SvgWidgets)
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Widgets Svg SvgWidgets Network WebSockets)
|
||||
|
||||
# Try to find the component-interfaces package first
|
||||
find_package(component-interfaces QUIET)
|
||||
@ -29,6 +29,8 @@ set(SOURCES
|
||||
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
|
||||
@ -71,6 +73,8 @@ target_link_libraries(lez_explorer_ui PRIVATE
|
||||
Qt6::Widgets
|
||||
Qt6::Svg
|
||||
Qt6::SvgWidgets
|
||||
Qt6::Network
|
||||
Qt6::WebSockets
|
||||
component-interfaces
|
||||
)
|
||||
|
||||
|
||||
@ -42,6 +42,8 @@
|
||||
];
|
||||
buildInputs = [
|
||||
pkgs.qt6.qtbase
|
||||
pkgs.qt6.qtsvg
|
||||
pkgs.qt6.qtwebsockets
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
|
||||
@ -15,6 +15,7 @@ pkgs.stdenv.mkDerivation rec {
|
||||
|
||||
qtLibPath = pkgs.lib.makeLibraryPath ([
|
||||
pkgs.qt6.qtbase
|
||||
pkgs.qt6.qtwebsockets
|
||||
] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [
|
||||
pkgs.libglvnd
|
||||
]);
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
buildInputs = [
|
||||
pkgs.qt6.qtbase
|
||||
pkgs.qt6.qtsvg
|
||||
pkgs.qt6.qtwebsockets
|
||||
];
|
||||
|
||||
cmakeFlags = [
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#include "ExplorerWidget.h"
|
||||
#include "Style.h"
|
||||
#include "services/MockIndexerService.h"
|
||||
#include "services/RpcIndexerService.h"
|
||||
#include "widgets/NavigationBar.h"
|
||||
#include "widgets/SearchBar.h"
|
||||
#include "pages/MainPage.h"
|
||||
@ -14,7 +14,7 @@
|
||||
|
||||
ExplorerWidget::ExplorerWidget(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_indexer(std::make_unique<MockIndexerService>())
|
||||
, m_indexer(std::make_unique<RpcIndexerService>())
|
||||
{
|
||||
setStyleSheet(Style::appBackground());
|
||||
|
||||
|
||||
@ -9,6 +9,9 @@
|
||||
#include <QFrame>
|
||||
#include <QPixmap>
|
||||
#include <QPushButton>
|
||||
#include <QLineEdit>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
MainPage::MainPage(IndexerService* indexer, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
@ -17,6 +20,31 @@ MainPage::MainPage(IndexerService* indexer, QWidget* parent)
|
||||
auto* outerLayout = new QVBoxLayout(this);
|
||||
outerLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
// Indexer endpoint selector
|
||||
auto* endpointRow = new QHBoxLayout();
|
||||
endpointRow->setSpacing(8);
|
||||
|
||||
auto* endpointLabel = new QLabel("Indexer RPC", 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->setMinimumHeight(34);
|
||||
m_indexerEndpointInput->setStyleSheet(Style::searchInput());
|
||||
|
||||
auto* applyEndpointBtn = new QPushButton("Apply", this);
|
||||
applyEndpointBtn->setMinimumHeight(34);
|
||||
applyEndpointBtn->setStyleSheet(Style::searchButton());
|
||||
|
||||
endpointRow->addWidget(endpointLabel);
|
||||
endpointRow->addWidget(m_indexerEndpointInput, 1);
|
||||
endpointRow->addWidget(applyEndpointBtn);
|
||||
outerLayout->addLayout(endpointRow);
|
||||
|
||||
connect(applyEndpointBtn, &QPushButton::clicked, this, &MainPage::applyIndexerEndpoint);
|
||||
connect(m_indexerEndpointInput, &QLineEdit::returnPressed, this, &MainPage::applyIndexerEndpoint);
|
||||
|
||||
// Health indicator
|
||||
auto* healthRow = new QHBoxLayout();
|
||||
auto* healthIcon = new QLabel(this);
|
||||
@ -72,11 +100,12 @@ QWidget* MainPage::createSectionHeader(const QString& title, const QString& icon
|
||||
return container;
|
||||
}
|
||||
|
||||
void MainPage::addBlockRow(QVBoxLayout* layout, const Block& block)
|
||||
void MainPage::addBlockRow(QVBoxLayout* layout, const Block& block, int insertIndex)
|
||||
{
|
||||
auto* frame = new ClickableFrame();
|
||||
frame->setFrameShape(QFrame::NoFrame);
|
||||
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
|
||||
frame->setProperty("blockId", QVariant::fromValue<qulonglong>(block.blockId));
|
||||
|
||||
auto* row = new QHBoxLayout(frame);
|
||||
row->setSpacing(10);
|
||||
@ -122,7 +151,63 @@ void MainPage::addBlockRow(QVBoxLayout* layout, const Block& block)
|
||||
emit blockClicked(blockId);
|
||||
});
|
||||
|
||||
layout->addWidget(frame);
|
||||
if (insertIndex >= 0) {
|
||||
layout->insertWidget(insertIndex, frame);
|
||||
} else {
|
||||
layout->addWidget(frame);
|
||||
}
|
||||
}
|
||||
|
||||
void MainPage::insertRecentBlock(const Block& block)
|
||||
{
|
||||
if (!m_blocksLayout || m_displayedBlockIds.contains(block.blockId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int insertIndex = m_blocksLayout->count();
|
||||
for (int i = 0; i < m_blocksLayout->count(); ++i) {
|
||||
QLayoutItem* item = m_blocksLayout->itemAt(i);
|
||||
if (!item || !item->widget()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
const quint64 existingId = item->widget()->property("blockId").toULongLong(&ok);
|
||||
if (ok && block.blockId > existingId) {
|
||||
insertIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
addBlockRow(m_blocksLayout, block, insertIndex);
|
||||
m_displayedBlockIds.insert(block.blockId);
|
||||
|
||||
if (!m_newestLoadedBlockId || block.blockId > *m_newestLoadedBlockId) {
|
||||
m_newestLoadedBlockId = block.blockId;
|
||||
}
|
||||
|
||||
if (!m_oldestLoadedBlockId || block.blockId < *m_oldestLoadedBlockId) {
|
||||
m_oldestLoadedBlockId = block.blockId;
|
||||
}
|
||||
}
|
||||
|
||||
QVector<Block> MainPage::fetchRecentBlocks(int limit)
|
||||
{
|
||||
if (limit <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto blocks = m_indexer->getBlocks(std::nullopt, limit);
|
||||
|
||||
std::sort(blocks.begin(), blocks.end(), [](const Block& lhs, const Block& rhs) {
|
||||
return lhs.blockId > rhs.blockId;
|
||||
});
|
||||
|
||||
if (blocks.size() > limit) {
|
||||
blocks.resize(limit);
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
void MainPage::addTransactionRow(QVBoxLayout* layout, const Transaction& tx)
|
||||
@ -211,8 +296,9 @@ void MainPage::refresh()
|
||||
{
|
||||
clearSearchResults();
|
||||
|
||||
quint64 latestId = m_indexer->getLatestBlockId();
|
||||
m_healthLabel->setText(QString("Chain height: %1").arg(latestId));
|
||||
quint64 latestId = m_indexer->getLastFinalizedBlockId();
|
||||
m_latestKnownBlockId = latestId;
|
||||
m_healthLabel->setText(QString("Chain height: %1").arg(m_latestKnownBlockId));
|
||||
m_healthLabel->setStyleSheet(Style::healthLabel());
|
||||
|
||||
if (m_recentBlocksWidget) {
|
||||
@ -223,7 +309,9 @@ void MainPage::refresh()
|
||||
m_loadMoreBtn = nullptr;
|
||||
}
|
||||
|
||||
m_newestLoadedBlockId = std::nullopt;
|
||||
m_oldestLoadedBlockId = std::nullopt;
|
||||
m_displayedBlockIds.clear();
|
||||
|
||||
m_recentBlocksWidget = new QWidget();
|
||||
auto* outerLayout = new QVBoxLayout(m_recentBlocksWidget);
|
||||
@ -236,11 +324,10 @@ void MainPage::refresh()
|
||||
m_blocksLayout->setContentsMargins(0, 0, 0, 0);
|
||||
outerLayout->addWidget(blockRowsWidget);
|
||||
|
||||
auto blocks = m_indexer->getBlocks(std::nullopt, 10);
|
||||
auto blocks = fetchRecentBlocks(10);
|
||||
|
||||
for (const auto& block : blocks) {
|
||||
addBlockRow(m_blocksLayout, block);
|
||||
if (!m_oldestLoadedBlockId || block.blockId < *m_oldestLoadedBlockId)
|
||||
m_oldestLoadedBlockId = block.blockId;
|
||||
insertRecentBlock(block);
|
||||
}
|
||||
|
||||
m_loadMoreBtn = new QPushButton("Load more");
|
||||
@ -258,13 +345,28 @@ void MainPage::loadMoreBlocks()
|
||||
return;
|
||||
|
||||
auto blocks = m_indexer->getBlocks(m_oldestLoadedBlockId, 10);
|
||||
std::sort(blocks.begin(), blocks.end(), [](const Block& lhs, const Block& rhs) {
|
||||
return lhs.blockId > rhs.blockId;
|
||||
});
|
||||
|
||||
for (const auto& block : blocks) {
|
||||
addBlockRow(m_blocksLayout, block);
|
||||
if (block.blockId < *m_oldestLoadedBlockId)
|
||||
m_oldestLoadedBlockId = block.blockId;
|
||||
insertRecentBlock(block);
|
||||
}
|
||||
|
||||
m_loadMoreBtn->setVisible(*m_oldestLoadedBlockId > 1 && !blocks.isEmpty());
|
||||
m_loadMoreBtn->setVisible(m_oldestLoadedBlockId && *m_oldestLoadedBlockId > 1 && !blocks.isEmpty());
|
||||
}
|
||||
|
||||
void MainPage::applyIndexerEndpoint()
|
||||
{
|
||||
QString endpoint = m_indexerEndpointInput->text().trimmed();
|
||||
if (endpoint.isEmpty()) {
|
||||
endpoint = IndexerService::defaultEndpoint();
|
||||
m_indexerEndpointInput->setText(endpoint);
|
||||
}
|
||||
|
||||
m_indexer->setEndpoint(endpoint);
|
||||
clearSearchResults();
|
||||
refresh();
|
||||
}
|
||||
|
||||
void MainPage::showSearchResults(const SearchResults& results)
|
||||
@ -319,19 +421,26 @@ void MainPage::onNewBlock(const Block& block)
|
||||
{
|
||||
if (!m_blocksLayout) return;
|
||||
|
||||
m_healthLabel->setText(QString("Chain height: %1").arg(block.blockId));
|
||||
m_latestKnownBlockId = std::max(m_latestKnownBlockId, block.blockId);
|
||||
m_healthLabel->setText(QString("Chain height: %1").arg(m_latestKnownBlockId));
|
||||
|
||||
// Append to layout then move to top (index 0)
|
||||
int countBefore = m_blocksLayout->count();
|
||||
addBlockRow(m_blocksLayout, block);
|
||||
if (m_blocksLayout->count() <= countBefore) return;
|
||||
// If notifications jump ahead, rebuild recent list from backend
|
||||
// so we do not mix one fresh head block with stale rows.
|
||||
if (!m_newestLoadedBlockId) {
|
||||
refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
QLayoutItem* item = m_blocksLayout->itemAt(m_blocksLayout->count() - 1);
|
||||
if (!item || !item->widget()) return;
|
||||
if (block.blockId > *m_newestLoadedBlockId && (block.blockId - *m_newestLoadedBlockId) > 1) {
|
||||
refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
QWidget* newRow = item->widget();
|
||||
m_blocksLayout->removeWidget(newRow);
|
||||
m_blocksLayout->insertWidget(0, newRow);
|
||||
insertRecentBlock(block);
|
||||
|
||||
if (m_loadMoreBtn) {
|
||||
m_loadMoreBtn->setVisible(m_oldestLoadedBlockId && *m_oldestLoadedBlockId > 1);
|
||||
}
|
||||
}
|
||||
|
||||
void MainPage::clearSearchResults()
|
||||
|
||||
@ -3,11 +3,13 @@
|
||||
#include "services/IndexerService.h"
|
||||
|
||||
#include <QWidget>
|
||||
#include <QSet>
|
||||
#include <optional>
|
||||
|
||||
class QVBoxLayout;
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
class QLineEdit;
|
||||
|
||||
class MainPage : public QWidget {
|
||||
Q_OBJECT
|
||||
@ -26,11 +28,14 @@ signals:
|
||||
void accountClicked(const QString& accountId);
|
||||
|
||||
private:
|
||||
void addBlockRow(QVBoxLayout* layout, const Block& block);
|
||||
void addBlockRow(QVBoxLayout* layout, const Block& block, int insertIndex = -1);
|
||||
void addTransactionRow(QVBoxLayout* layout, const Transaction& tx);
|
||||
void addAccountRow(QVBoxLayout* layout, const Account& account);
|
||||
QWidget* createSectionHeader(const QString& title, const QString& iconPath = {});
|
||||
QVector<Block> fetchRecentBlocks(int limit);
|
||||
void insertRecentBlock(const Block& block);
|
||||
void loadMoreBlocks();
|
||||
void applyIndexerEndpoint();
|
||||
|
||||
IndexerService* m_indexer = nullptr;
|
||||
QVBoxLayout* m_contentLayout = nullptr;
|
||||
@ -39,5 +44,9 @@ private:
|
||||
QVBoxLayout* m_blocksLayout = nullptr;
|
||||
QPushButton* m_loadMoreBtn = nullptr;
|
||||
QLabel* m_healthLabel = nullptr;
|
||||
QLineEdit* m_indexerEndpointInput = nullptr;
|
||||
std::optional<quint64> m_newestLoadedBlockId;
|
||||
std::optional<quint64> m_oldestLoadedBlockId;
|
||||
QSet<quint64> m_displayedBlockIds;
|
||||
quint64 m_latestKnownBlockId = 0;
|
||||
};
|
||||
|
||||
@ -21,12 +21,20 @@ public:
|
||||
explicit IndexerService(QObject* parent = nullptr) : QObject(parent) {}
|
||||
~IndexerService() override = default;
|
||||
|
||||
static QString defaultEndpoint()
|
||||
{
|
||||
return "ws://localhost:8779";
|
||||
}
|
||||
|
||||
virtual QString endpoint() const = 0;
|
||||
virtual void setEndpoint(const QString& endpoint) = 0;
|
||||
|
||||
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 quint64 getLastFinalizedBlockId() = 0;
|
||||
virtual QVector<Transaction> getTransactionsByAccount(const QString& accountId, int offset, int limit) = 0;
|
||||
virtual SearchResults search(const QString& query) = 0;
|
||||
|
||||
|
||||
@ -40,6 +40,19 @@ MockIndexerService::MockIndexerService(QObject* parent)
|
||||
m_blockTimer.start(30000); // 30 seconds
|
||||
}
|
||||
|
||||
QString MockIndexerService::endpoint() const
|
||||
{
|
||||
return m_endpoint;
|
||||
}
|
||||
|
||||
void MockIndexerService::setEndpoint(const QString& endpoint)
|
||||
{
|
||||
m_endpoint = endpoint.trimmed();
|
||||
if (m_endpoint.isEmpty()) {
|
||||
m_endpoint = IndexerService::defaultEndpoint();
|
||||
}
|
||||
}
|
||||
|
||||
Block MockIndexerService::generateBlock(quint64 blockId, const QString& prevHash)
|
||||
{
|
||||
auto* rng = QRandomGenerator::global();
|
||||
@ -303,7 +316,7 @@ QVector<Block> MockIndexerService::getBlocks(std::optional<quint64> before, int
|
||||
return result;
|
||||
}
|
||||
|
||||
quint64 MockIndexerService::getLatestBlockId()
|
||||
quint64 MockIndexerService::getLastFinalizedBlockId()
|
||||
{
|
||||
if (m_blocks.isEmpty()) return 0;
|
||||
return m_blocks.last().blockId;
|
||||
|
||||
@ -11,12 +11,15 @@ class MockIndexerService : public IndexerService {
|
||||
public:
|
||||
explicit MockIndexerService(QObject* parent = nullptr);
|
||||
|
||||
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 getLatestBlockId() override;
|
||||
quint64 getLastFinalizedBlockId() override;
|
||||
QVector<Transaction> getTransactionsByAccount(const QString& accountId, int offset, int limit) override;
|
||||
SearchResults search(const QString& query) override;
|
||||
|
||||
@ -37,4 +40,5 @@ private:
|
||||
QMap<QString, Transaction> m_transactionsByHash;
|
||||
QMap<QString, Account> m_accounts;
|
||||
QTimer m_blockTimer;
|
||||
QString m_endpoint = IndexerService::defaultEndpoint();
|
||||
};
|
||||
|
||||
741
src/services/RpcIndexerService.cpp
Normal file
741
src/services/RpcIndexerService.cpp
Normal file
@ -0,0 +1,741 @@
|
||||
#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();
|
||||
}
|
||||
66
src/services/RpcIndexerService.h
Normal file
66
src/services/RpcIndexerService.h
Normal file
@ -0,0 +1,66 @@
|
||||
#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