mirror of
https://github.com/logos-blockchain/lez-indexer-module.git
synced 2026-07-29 22:53:28 +00:00
Merge pull request #10 from logos-blockchain/erhant/migr-to-logos-module-builder
feat!: migrate to logos-module-builder
This commit is contained in:
commit
385c0ce6a0
248
CMakeLists.txt
248
CMakeLists.txt
@ -1,228 +1,36 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(LEZIndexerModule LANGUAGES CXX)
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(LezIndexerModulePlugin LANGUAGES CXX)
|
||||
|
||||
# __uint128_t (u128 balances/nonces) is a GCC/Clang extension; C++20 to match
|
||||
# the rest of the Logos C++ stack. nlohmann::json needs C++11+.
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# ---- Options ----
|
||||
option(LOGOS_MODULE_BUNDLE "Create local runtime bundle in target/." ON)
|
||||
set(LOGOS_CORE_ROOT "" CACHE PATH "Path to logos-core root directory.")
|
||||
set(LOGOS_EXECUTION_ZONE_INDEXER_ROOT "" CACHE PATH "Path to logos-execution-zone-indexer source root.")
|
||||
set(LOGOS_EXECUTION_ZONE_INDEXER_LIB "" CACHE PATH "Path to prebuilt logos-execution-zone-indexer lib.")
|
||||
set(LOGOS_EXECUTION_ZONE_INDEXER_INCLUDE "" CACHE PATH "Path to prebuilt logos-execution-zone-indexer include.")
|
||||
|
||||
set(LOGOS_EXECUTION_ZONE_PREBUILT FALSE)
|
||||
|
||||
set(HAS_LOGOS_CORE_ROOT FALSE)
|
||||
set(HAS_LOGOS_EXECUTION_ZONE_INDEXER_ROOT FALSE)
|
||||
set(HAS_LOGOS_EXECUTION_ZONE_INDEXER_LIB FALSE)
|
||||
set(HAS_LOGOS_EXECUTION_ZONE_INDEXER_INCLUDE FALSE)
|
||||
|
||||
if (DEFINED LOGOS_CORE_ROOT AND NOT "${LOGOS_CORE_ROOT}" STREQUAL "")
|
||||
set(HAS_LOGOS_CORE_ROOT TRUE)
|
||||
endif()
|
||||
|
||||
if (DEFINED LOGOS_EXECUTION_ZONE_INDEXER_ROOT AND NOT "${LOGOS_EXECUTION_ZONE_INDEXER_ROOT}" STREQUAL "")
|
||||
set(HAS_LOGOS_EXECUTION_ZONE_INDEXER_ROOT TRUE)
|
||||
endif()
|
||||
|
||||
if(DEFINED LOGOS_EXECUTION_ZONE_INDEXER_LIB AND NOT "${LOGOS_EXECUTION_ZONE_INDEXER_LIB}" STREQUAL "")
|
||||
set(HAS_LOGOS_EXECUTION_ZONE_INDEXER_LIB TRUE)
|
||||
endif()
|
||||
|
||||
if(DEFINED LOGOS_EXECUTION_ZONE_INDEXER_INCLUDE AND NOT "${LOGOS_EXECUTION_ZONE_INDEXER_INCLUDE}" STREQUAL "")
|
||||
set(HAS_LOGOS_EXECUTION_ZONE_INDEXER_INCLUDE TRUE)
|
||||
endif()
|
||||
|
||||
if (NOT HAS_LOGOS_CORE_ROOT)
|
||||
message(FATAL_ERROR "LOGOS_CORE_ROOT must be set to the logos-core root directory.")
|
||||
endif()
|
||||
|
||||
if(HAS_LOGOS_EXECUTION_ZONE_INDEXER_LIB AND HAS_LOGOS_EXECUTION_ZONE_INDEXER_INCLUDE AND NOT HAS_LOGOS_EXECUTION_ZONE_INDEXER_ROOT)
|
||||
message(STATUS "Using prebuilt logos-execution-zone-indexer.")
|
||||
set(LOGOS_EXECUTION_ZONE_PREBUILT TRUE)
|
||||
elseif(NOT HAS_LOGOS_EXECUTION_ZONE_INDEXER_LIB AND NOT HAS_LOGOS_EXECUTION_ZONE_INDEXER_INCLUDE AND HAS_LOGOS_EXECUTION_ZONE_INDEXER_ROOT)
|
||||
message(STATUS "Building logos-execution-zone-indexer from source.")
|
||||
set(LOGOS_EXECUTION_ZONE_PREBUILT FALSE)
|
||||
# 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()
|
||||
message(FATAL_ERROR "Either both LOGOS_EXECUTION_ZONE_INDEXER_LIB and LOGOS_EXECUTION_ZONE_INDEXER_INCLUDE must be set for prebuilt logos-execution-zone-indexer, or LOGOS_EXECUTION_ZONE_INDEXER_ROOT must be set to build from source.")
|
||||
message(FATAL_ERROR "LogosModule.cmake not found. Set LOGOS_MODULE_BUILDER_ROOT.")
|
||||
endif()
|
||||
|
||||
# ---- Qt ----
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core RemoteObjects)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
|
||||
# ---- OpenSSL ----
|
||||
# liblogos_sdk.a uses boost::asio::ssl (rpc_server, plain_transport_*); as a
|
||||
# static archive it does not carry its OpenSSL dependency, so the final link
|
||||
# of this plugin must link libssl/libcrypto itself.
|
||||
find_package(OpenSSL REQUIRED)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
# ---- Directories ----
|
||||
set(WORKSPACE_ROOT "${CMAKE_BINARY_DIR}/workspace")
|
||||
file(MAKE_DIRECTORY "${WORKSPACE_ROOT}")
|
||||
|
||||
# ---- Logos Core SDK ----
|
||||
set(SDK_LIB "${LOGOS_CORE_ROOT}/lib/liblogos_sdk.a")
|
||||
set(SDK_INC "${LOGOS_CORE_ROOT}/include")
|
||||
|
||||
# ---- OS Specifics ----
|
||||
if(APPLE)
|
||||
set(DYLIB_EXT ".dylib")
|
||||
elseif(WIN32)
|
||||
set(DYLIB_EXT ".dll")
|
||||
set(IMPLIB_EXT ".lib")
|
||||
else()
|
||||
set(DYLIB_EXT ".so")
|
||||
endif()
|
||||
|
||||
# NOTE (Windows):
|
||||
# Rust cdylib typically produces:
|
||||
# - logos_blockchain.dll (runtime)
|
||||
# - logos_blockchain.lib (import lib)
|
||||
# The Windows build hasn't been yet, so adjust accordingly if the DLL is named without the 'lib' prefix.
|
||||
|
||||
# ---- Logos Blockchain (build OR consume) ----
|
||||
if(LOGOS_EXECUTION_ZONE_PREBUILT)
|
||||
set(LOGOS_EXECUTION_ZONE_DYLIB "${LOGOS_EXECUTION_ZONE_INDEXER_LIB}/libindexer_ffi${DYLIB_EXT}")
|
||||
|
||||
if(WIN32)
|
||||
set(LOGOS_EXECUTION_ZONE_IMPLIB "${LOGOS_EXECUTION_ZONE_INDEXER_LIB}/indexer_ffi${IMPLIB_EXT}")
|
||||
endif()
|
||||
|
||||
add_custom_target(logos_execution_zone_libs)
|
||||
|
||||
else()
|
||||
find_program(CARGO_EXECUTABLE cargo REQUIRED)
|
||||
|
||||
set(CARGO_TARGET_DIR "${WORKSPACE_ROOT}/logos-execution-zone/target")
|
||||
set(INTERNAL_STAGE "${WORKSPACE_ROOT}/stage")
|
||||
set(INTERNAL_STAGE_LIB "${INTERNAL_STAGE}/lib")
|
||||
set(INTERNAL_STAGE_INCLUDE "${INTERNAL_STAGE}/include")
|
||||
file(MAKE_DIRECTORY "${CARGO_TARGET_DIR}" "${INTERNAL_STAGE_LIB}" "${INTERNAL_STAGE_INCLUDE}")
|
||||
|
||||
set(LOGOS_EXECUTION_ZONE_INDEXER_LIB "${INTERNAL_STAGE_LIB}")
|
||||
set(LOGOS_EXECUTION_ZONE_INDEXER_INCLUDE "${INTERNAL_STAGE_INCLUDE}")
|
||||
|
||||
set(LOGOS_EXECUTION_ZONE_DYLIB "${INTERNAL_STAGE_LIB}/libindexer_ffi${DYLIB_EXT}")
|
||||
set(LOGOS_EXECUTION_ZONE_HEADER "${INTERNAL_STAGE_INCLUDE}/indexer_ffi.h")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${LOGOS_EXECUTION_ZONE_DYLIB}"
|
||||
COMMAND ${CMAKE_COMMAND} -E env
|
||||
CARGO_TARGET_DIR=${CARGO_TARGET_DIR}
|
||||
${CARGO_EXECUTABLE} build --release
|
||||
--package indexer-ffi
|
||||
--manifest-path "${LOGOS_EXECUTION_ZONE_INDEXER_ROOT}/Cargo.toml"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
"${CARGO_TARGET_DIR}/release/libindexer_ffi${DYLIB_EXT}"
|
||||
"${LOGOS_EXECUTION_ZONE_DYLIB}"
|
||||
DEPENDS "${LOGOS_EXECUTION_ZONE_INDEXER_ROOT}/Cargo.toml"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${LOGOS_EXECUTION_ZONE_HEADER}"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${LOGOS_EXECUTION_ZONE_INDEXER_ROOT}/indexer-ffi/indexer_ffi.h"
|
||||
"${LOGOS_EXECUTION_ZONE_HEADER}"
|
||||
DEPENDS "${LOGOS_EXECUTION_ZONE_DYLIB}"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
add_custom_target(logos_execution_zone_libs DEPENDS "${LOGOS_EXECUTION_ZONE_DYLIB}" "${LOGOS_EXECUTION_ZONE_HEADER}")
|
||||
endif()
|
||||
|
||||
# ---- Imported targets ----
|
||||
add_library(logos_execution_zone_indexer SHARED IMPORTED GLOBAL)
|
||||
set_target_properties(logos_execution_zone_indexer PROPERTIES
|
||||
IMPORTED_LOCATION "${LOGOS_EXECUTION_ZONE_DYLIB}"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${LOGOS_EXECUTION_ZONE_INDEXER_INCLUDE}"
|
||||
# Universal core module: we write only the impl class + marshalling. The plugin
|
||||
# glue is generated and compiled automatically. The logos SDK / protocol (and
|
||||
# its transitive nlohmann_json + OpenSSL + Boost) are linked by logos_module().
|
||||
# EXTERNAL_LIBS indexer_ffi links libindexer_ffi from the staged lib/ dir and
|
||||
# adds lib/ to the include path so <indexer_ffi.h> resolves.
|
||||
logos_module(
|
||||
NAME lez_indexer_module
|
||||
SOURCES
|
||||
src/lez_indexer_module_impl.h
|
||||
src/lez_indexer_module_impl.cpp
|
||||
src/lez_ffi_marshalling.h
|
||||
src/lez_ffi_marshalling.cpp
|
||||
EXTERNAL_LIBS
|
||||
indexer_ffi
|
||||
INCLUDE_DIRS
|
||||
src
|
||||
lib
|
||||
)
|
||||
|
||||
if(NOT LOGOS_EXECUTION_ZONE_PREBUILT)
|
||||
add_dependencies(logos_execution_zone_indexer logos_execution_zone_libs)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
set_target_properties(logos_execution_zone_indexer PROPERTIES IMPORTED_IMPLIB "${LOGOS_EXECUTION_ZONE_IMPLIB}")
|
||||
endif()
|
||||
|
||||
add_library(logos_core STATIC IMPORTED
|
||||
src/i_lez_indexer_module.h)
|
||||
set_target_properties(logos_core PROPERTIES
|
||||
IMPORTED_LOCATION "${SDK_LIB}"
|
||||
)
|
||||
|
||||
add_library(logos_cpp_sdk INTERFACE)
|
||||
target_include_directories(logos_cpp_sdk INTERFACE "${SDK_INC}")
|
||||
|
||||
# ---- Plugin ----
|
||||
set(PLUGIN_TARGET lez_indexer_module)
|
||||
|
||||
qt_add_plugin(${PLUGIN_TARGET} CLASS_NAME LezIndexerModule)
|
||||
|
||||
# A consumer (module-viewer / basecamp) derives the QtRO object name it INVOKES
|
||||
# from the produced library's FILENAME, whereas logos_host REGISTERS the plugin
|
||||
# under metadata.json `name`. They must be identical, and short — macOS caps the
|
||||
# QtRO `local:` socket path (sun_path) at 104 bytes, so a long name makes the host
|
||||
# fail to listen. `PREFIX ""` drops the `lib` prefix so the file is exactly
|
||||
# `lez_indexer_module.<ext>` == metadata `name` == `main` == name().
|
||||
set_target_properties(${PLUGIN_TARGET} PROPERTIES
|
||||
OUTPUT_NAME lez_indexer_module
|
||||
PREFIX ""
|
||||
)
|
||||
|
||||
target_sources(${PLUGIN_TARGET} PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/lez_indexer_module.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/lez_ffi_marshalling.cpp
|
||||
)
|
||||
|
||||
set_property(TARGET ${PLUGIN_TARGET} PROPERTY PUBLIC_HEADER
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/i_lez_indexer_module.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/lez_indexer_module.h
|
||||
)
|
||||
|
||||
target_include_directories(${PLUGIN_TARGET} PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
)
|
||||
|
||||
target_link_libraries(${PLUGIN_TARGET} PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::RemoteObjects
|
||||
logos_execution_zone_indexer
|
||||
logos_cpp_sdk
|
||||
logos_core
|
||||
OpenSSL::SSL
|
||||
OpenSSL::Crypto
|
||||
)
|
||||
|
||||
target_compile_definitions(${PLUGIN_TARGET} PRIVATE
|
||||
LEZ_INDEXER_MODULE_METADATA_FILE="${CMAKE_CURRENT_SOURCE_DIR}/metadata.json"
|
||||
)
|
||||
|
||||
add_dependencies(${PLUGIN_TARGET} logos_execution_zone_libs)
|
||||
|
||||
if(APPLE)
|
||||
set_target_properties(${PLUGIN_TARGET} PROPERTIES
|
||||
BUILD_RPATH "@loader_path"
|
||||
INSTALL_RPATH "@loader_path"
|
||||
)
|
||||
elseif(UNIX)
|
||||
set_target_properties(${PLUGIN_TARGET} PROPERTIES
|
||||
BUILD_RPATH "$ORIGIN"
|
||||
INSTALL_RPATH "$ORIGIN"
|
||||
)
|
||||
endif()
|
||||
|
||||
# ---- Install ----
|
||||
install(TARGETS ${PLUGIN_TARGET}
|
||||
LIBRARY DESTINATION lib
|
||||
ARCHIVE DESTINATION lib
|
||||
RUNTIME DESTINATION bin
|
||||
PUBLIC_HEADER DESTINATION include
|
||||
)
|
||||
install(DIRECTORY "${LOGOS_EXECUTION_ZONE_INDEXER_INCLUDE}/" DESTINATION include)
|
||||
install(FILES "${LOGOS_EXECUTION_ZONE_DYLIB}" DESTINATION lib)
|
||||
|
||||
51
README.md
51
README.md
@ -1,14 +1,14 @@
|
||||
# Logos Execution Zone Indexer Module
|
||||
|
||||
A Logos Core **service module** (`type: core`) that runs the Logos Execution Zone (L2) indexer and exposes it to the Logos ecosystem. It is a thin Qt plugin around the `indexer_ffi` library from
|
||||
[`logos-execution-zone`](https://github.com/logos-blockchain/logos-execution-zone): it starts the indexer, which connects to an L1/bedrock node, indexes the zone's channel, and serves queries over an RPC (WebSocket) server.
|
||||
A Logos Core **service module** (`type: core`, universal authoring model) that runs the Logos Execution Zone (L2) indexer and exposes it to the Logos ecosystem. It is a thin Qt-free plugin around the `indexer_ffi` library from
|
||||
[`logos-execution-zone`](https://github.com/logos-blockchain/logos-execution-zone): it starts the indexer, which connects to an L1/bedrock node and indexes the zone's channel, and exposes **query methods over the Logos protocol** (Qt Remote Objects).
|
||||
|
||||
Registered module name: **`lez_indexer_module`**. It pairs with the
|
||||
[`lez-explorer-ui`](https://github.com/logos-co/lez-explorer-ui) block explorer, which connects to the indexer's RPC endpoint.
|
||||
Registered module name: **`lez_indexer_module`**. Its public methods _are_ its API — other modules call them in-process over the Logos protocol. It pairs with the
|
||||
[`lez-explorer-ui`](https://github.com/logos-co/lez-explorer-ui) block explorer, which reads from it **in-process over the Logos protocol** (typed `modules().lez_indexer_module.*` calls) — no RPC endpoint or socket in between.
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> **Keep the module name short.** `logos_host` derives a Qt Remote Objects `local:` socket from it (`local:logos_<name>_<id>`), and Unix socket path names are limited in at most 108 bytes (see [man unix](https://man7.org/linux/man-pages/man7/unix.7.html)). The build emits the library with no `lib` prefix so its filename equals the registered name (`lez_indexer_module`) - consumers derive the QtRO invoke target from the filename, so the two must stay identical.
|
||||
> **Keep the module name short.** `logos_host` derives a Qt Remote Objects `local:` socket from it (`local:logos_<name>_<id>`), and Unix socket path names are limited to at most 108 bytes (see [man unix](https://man7.org/linux/man-pages/man7/unix.7.html)). The build emits the library with no `lib` prefix so its filename equals the registered name (`lez_indexer_module`) — consumers derive the QtRO invoke target from the filename, so the two must stay identical.
|
||||
|
||||
## Setup
|
||||
|
||||
@ -27,30 +27,49 @@ This will reduce friction when working on the project.
|
||||
|
||||
## Usage
|
||||
|
||||
The module does **not** start the indexer on load - something must invoke its `start_indexer` method (via the module-viewer's invoke panel, or another module / basecamp over the Logos API):
|
||||
The module does **not** start the indexer on load — something must invoke its `start_indexer` method (via the module-viewer's invoke panel, or another module / basecamp over the Logos protocol):
|
||||
|
||||
```c
|
||||
start_indexer(config_path, port)
|
||||
start_indexer(config_path)
|
||||
```
|
||||
|
||||
- `config_path` — **absolute** path to a JSON config (see
|
||||
[`config/indexer_config.json`](config/indexer_config.json)). It must be absolute: the module runs inside the `logos_host` subprocess, whose working directory is not your shell's.
|
||||
- `port` — TCP port for the RPC server, e.g. `8779`. Passed as a **string** (see the macOS / Qt notes below).
|
||||
|
||||
On success it returns `0` and the RPC server listens on `ws://localhost:<port>`; point the explorer (or any client) there.
|
||||
On success it returns `0`; a non-zero return is the FFI `OperationStatus` (e.g. `2 = InitializationError`). Once started, consume the indexer through the query methods below.
|
||||
|
||||
> [!CAUTION]
|
||||
> [!TIP]
|
||||
>
|
||||
> A non-zero return is the FFI `OperationStatus` (e.g. `2 = InitializationError`) — note the FFI does not log, so the numeric code is all you get.
|
||||
> By default the indexer is silent. Call `init_logger(level)` once (e.g.
|
||||
> `init_logger("info")`; accepts `off`/`error`/`warn`/`info`/`debug`/`trace`) to
|
||||
> surface the indexer's own `log` output — useful since a failed call otherwise
|
||||
> only reports a numeric `OperationStatus`. Logging is scoped to the indexer
|
||||
> crates, and the first call wins.
|
||||
|
||||
### Query methods (the Logos-protocol API)
|
||||
|
||||
Each returns a compact JSON string (an **empty** string means not-found / failed query); all ids/hashes are 32-byte hex, all numeric args/ids are decimal strings.
|
||||
|
||||
| Method | Returns |
|
||||
| ----------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| `getStatus()` | indexer status JSON (schema owned by `indexer_core`) |
|
||||
| `getLastFinalizedBlockId()` | tip block id (bare decimal string) |
|
||||
| `getBlockById(block_id)` | block JSON |
|
||||
| `getBlockByHash(hash)` | block JSON |
|
||||
| `getBlocks(before, limit)` | JSON array of blocks; `before` = `""` for the tip, else a block id to page back from |
|
||||
| `getTransaction(hash)` | transaction JSON |
|
||||
| `getAccount(account_id)` | account JSON (the payload omits the id; callers inject the queried id) |
|
||||
| `getTransactionsByAccount(account_id, offset, limit)` | JSON array of transactions touching the account |
|
||||
|
||||
Because the module uses the universal authoring model (`interface: "universal"`), it publishes a typed LIDL contract, so universal consumers get Qt-typed wrappers (`modules().lez_indexer_module.getBlockById(...)`) rather than dynamic by-name calls.
|
||||
|
||||
### Configuration
|
||||
|
||||
`config/indexer_config.json` is deserialized into the indexer's `IndexerConfig`. Key fields:
|
||||
|
||||
| Field | Meaning | Default |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- |
|
||||
| `bedrock_config.addr` | L1/bedrock node URL the indexer reads from; it must be reachable. | `http://localhost:8080` |
|
||||
| `home` | Directory for the indexer's RocksDB state, relative to the host's working directory. Use an absolute path for a predictable location. | `"."` |
|
||||
| `channel_id` | The zone channel the indexer consumes; must match what the sequencer inscribes. | |
|
||||
| Field | Meaning | Default |
|
||||
| --------------------- | ------------------------------------------------------------------------------- | ----------------------- |
|
||||
| `bedrock_config.addr` | L1/bedrock node URL the indexer reads from; it must be reachable. | `http://localhost:8080` |
|
||||
| `channel_id` | The zone channel the indexer consumes; must match what the sequencer inscribes. | |
|
||||
|
||||
The config keys must match the `IndexerConfig` schema of the `logos-execution-zone` rev pinned in `flake.nix`/`flake.lock`; bumping that rev may require re-syncing this file. Unknown keys are ignored.
|
||||
|
||||
@ -1,160 +1,53 @@
|
||||
{
|
||||
"home": ".",
|
||||
"consensus_info_polling_interval": "1s",
|
||||
"bedrock_config": {
|
||||
"addr": "http://localhost:8080",
|
||||
"backoff": {
|
||||
"start_delay": "100ms",
|
||||
"max_retries": 5
|
||||
}
|
||||
"consensus_info_polling_interval": "1s",
|
||||
"bedrock_config": {
|
||||
"addr": "http://localhost:8080",
|
||||
"backoff": {
|
||||
"start_delay": "100ms",
|
||||
"max_retries": 5
|
||||
}
|
||||
},
|
||||
"channel_id": "0101010101010101010101010101010101010101010101010101010101010101",
|
||||
"initial_accounts": [
|
||||
{
|
||||
"account_id": "CbgR6tj5kWx5oziiFptM7jMvrQeYY3Mzaao6ciuhSr2r",
|
||||
"balance": 10000
|
||||
},
|
||||
"channel_id": "0101010101010101010101010101010101010101010101010101010101010101",
|
||||
"initial_accounts": [
|
||||
{
|
||||
"account_id": "CbgR6tj5kWx5oziiFptM7jMvrQeYY3Mzaao6ciuhSr2r",
|
||||
"balance": 10000
|
||||
},
|
||||
{
|
||||
"account_id": "2RHZhw9h534Zr3eq2RGhQete2Hh667foECzXPmSkGni2",
|
||||
"balance": 20000
|
||||
}
|
||||
],
|
||||
"initial_commitments": [
|
||||
{
|
||||
"npk": [
|
||||
139,
|
||||
19,
|
||||
158,
|
||||
11,
|
||||
155,
|
||||
231,
|
||||
85,
|
||||
206,
|
||||
132,
|
||||
228,
|
||||
220,
|
||||
114,
|
||||
145,
|
||||
89,
|
||||
113,
|
||||
156,
|
||||
238,
|
||||
142,
|
||||
242,
|
||||
74,
|
||||
182,
|
||||
91,
|
||||
43,
|
||||
100,
|
||||
6,
|
||||
190,
|
||||
31,
|
||||
15,
|
||||
31,
|
||||
88,
|
||||
96,
|
||||
204
|
||||
],
|
||||
"account": {
|
||||
"program_owner": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"balance": 10000,
|
||||
"data": [],
|
||||
"nonce": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"npk": [
|
||||
173,
|
||||
134,
|
||||
33,
|
||||
223,
|
||||
54,
|
||||
226,
|
||||
10,
|
||||
71,
|
||||
215,
|
||||
254,
|
||||
143,
|
||||
172,
|
||||
24,
|
||||
244,
|
||||
243,
|
||||
208,
|
||||
65,
|
||||
112,
|
||||
118,
|
||||
70,
|
||||
217,
|
||||
240,
|
||||
69,
|
||||
100,
|
||||
129,
|
||||
3,
|
||||
121,
|
||||
25,
|
||||
213,
|
||||
132,
|
||||
42,
|
||||
45
|
||||
],
|
||||
"account": {
|
||||
"program_owner": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"balance": 20000,
|
||||
"data": [],
|
||||
"nonce": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"signing_key": [
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37,
|
||||
37
|
||||
]
|
||||
}
|
||||
{
|
||||
"account_id": "2RHZhw9h534Zr3eq2RGhQete2Hh667foECzXPmSkGni2",
|
||||
"balance": 20000
|
||||
}
|
||||
],
|
||||
"initial_commitments": [
|
||||
{
|
||||
"npk": [
|
||||
139, 19, 158, 11, 155, 231, 85, 206, 132, 228, 220, 114, 145, 89, 113,
|
||||
156, 238, 142, 242, 74, 182, 91, 43, 100, 6, 190, 31, 15, 31, 88, 96,
|
||||
204
|
||||
],
|
||||
"account": {
|
||||
"program_owner": [0, 0, 0, 0, 0, 0, 0, 0],
|
||||
"balance": 10000,
|
||||
"data": [],
|
||||
"nonce": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"npk": [
|
||||
173, 134, 33, 223, 54, 226, 10, 71, 215, 254, 143, 172, 24, 244, 243,
|
||||
208, 65, 112, 118, 70, 217, 240, 69, 100, 129, 3, 121, 25, 213, 132, 42,
|
||||
45
|
||||
],
|
||||
"account": {
|
||||
"program_owner": [0, 0, 0, 0, 0, 0, 0, 0],
|
||||
"balance": 20000,
|
||||
"data": [],
|
||||
"nonce": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"signing_key": [
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37
|
||||
]
|
||||
}
|
||||
|
||||
11683
flake.lock
generated
11683
flake.lock
generated
File diff suppressed because it is too large
Load Diff
160
flake.nix
160
flake.nix
@ -1,150 +1,30 @@
|
||||
{
|
||||
description = "Logos Blockchain Module - Qt6 Plugin";
|
||||
description = "Logos Execution Zone Indexer Module (universal core, logos-module-builder)";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.follows = "logos-liblogos/nixpkgs";
|
||||
logos-module-builder.url = "github:logos-co/logos-module-builder";
|
||||
|
||||
logos-liblogos.url = "github:logos-co/logos-liblogos";
|
||||
logos-core.url = "github:logos-co/logos-cpp-sdk";
|
||||
|
||||
logos-execution-zone.url = "github:logos-blockchain/logos-execution-zone?ref=main";
|
||||
|
||||
logos-module-viewer.url = "github:logos-co/logos-module-viewer";
|
||||
# The LEZ indexer Rust FFI lib + header come from this flake's `indexer`
|
||||
# package output (/lib/libindexer_ffi.* + /include/indexer_ffi.h). It is a
|
||||
# prebuilt Nix derivation, so mkExternalLib consumes it directly (no build).
|
||||
logos-execution-zone.url = "git+https://github.com/logos-blockchain/logos-execution-zone?ref=main";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
logos-core,
|
||||
logos-execution-zone,
|
||||
logos-module-viewer,
|
||||
...
|
||||
}:
|
||||
let
|
||||
lib = nixpkgs.lib;
|
||||
|
||||
systems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
"aarch64-darwin"
|
||||
"x86_64-windows"
|
||||
];
|
||||
|
||||
forAll = lib.genAttrs systems;
|
||||
|
||||
mkPkgs = system: import nixpkgs { inherit system; };
|
||||
in
|
||||
{
|
||||
packages = forAll (
|
||||
system:
|
||||
let
|
||||
pkgs = mkPkgs system;
|
||||
llvmPkgs = pkgs.llvmPackages;
|
||||
|
||||
logosCore = logos-core.packages.${system}.default;
|
||||
logosExecutionZoneIndexerPackage = logos-execution-zone.packages.${system}.indexer;
|
||||
|
||||
lezIndexerModulePackage = pkgs.stdenv.mkDerivation {
|
||||
pname = "lez-indexer-module";
|
||||
version = (lib.importJSON ./metadata.json).version;
|
||||
src = ./.;
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkgs.cmake
|
||||
pkgs.ninja
|
||||
pkgs.pkg-config
|
||||
pkgs.qt6.wrapQtAppsHook
|
||||
];
|
||||
|
||||
buildInputs = [
|
||||
pkgs.qt6.qtbase
|
||||
pkgs.qt6.qtremoteobjects
|
||||
pkgs.qt6.qttools
|
||||
pkgs.openssl
|
||||
llvmPkgs.clang
|
||||
llvmPkgs.libclang
|
||||
logosExecutionZoneIndexerPackage
|
||||
]
|
||||
++ lib.optionals pkgs.stdenv.isDarwin [
|
||||
pkgs.libiconv
|
||||
pkgs.cacert
|
||||
];
|
||||
|
||||
LIBCLANG_PATH = "${llvmPkgs.libclang.lib}/lib";
|
||||
CLANG_PATH = "${llvmPkgs.clang}/bin/clang";
|
||||
SSL_CERT_FILE = lib.optionalString pkgs.stdenv.isDarwin "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
|
||||
|
||||
cmakeFlags = [
|
||||
"-DLOGOS_CORE_ROOT=${logosCore}"
|
||||
"-DLOGOS_EXECUTION_ZONE_INDEXER_LIB=${logosExecutionZoneIndexerPackage}/lib"
|
||||
"-DLOGOS_EXECUTION_ZONE_INDEXER_INCLUDE=${logosExecutionZoneIndexerPackage}/include"
|
||||
];
|
||||
inputs@{ logos-module-builder, logos-execution-zone, ... }:
|
||||
logos-module-builder.lib.mkLogosModule {
|
||||
src = ./.;
|
||||
configFile = ./metadata.json;
|
||||
flakeInputs = inputs;
|
||||
externalLibInputs = {
|
||||
# Structured form: the dep exposes its lib under packages.<system>.indexer
|
||||
# (not .default), so map the default build variant to that package.
|
||||
indexer_ffi = {
|
||||
input = logos-execution-zone;
|
||||
packages = {
|
||||
default = "indexer";
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
lib = lezIndexerModulePackage;
|
||||
default = lezIndexerModulePackage;
|
||||
}
|
||||
);
|
||||
|
||||
apps = forAll (
|
||||
system:
|
||||
let
|
||||
pkgs = mkPkgs system;
|
||||
lezIndexerModuleLib = self.packages.${system}.lib;
|
||||
logosModuleViewerPackage = logos-module-viewer.packages.${system}.default;
|
||||
extension = if pkgs.stdenv.isDarwin then "dylib"
|
||||
else if pkgs.stdenv.hostPlatform.isWindows then "dll"
|
||||
else "so";
|
||||
inspectModule = {
|
||||
type = "app";
|
||||
program =
|
||||
"${pkgs.writeShellScriptBin "inspect-module" ''
|
||||
exec ${logosModuleViewerPackage}/bin/logos-module-viewer \
|
||||
--module ${lezIndexerModuleLib}/lib/lez_indexer_module.${extension}
|
||||
''}/bin/inspect-module";
|
||||
};
|
||||
in
|
||||
{
|
||||
inspect-module = inspectModule;
|
||||
default = inspectModule;
|
||||
}
|
||||
);
|
||||
|
||||
devShells = forAll (
|
||||
system:
|
||||
let
|
||||
pkgs = mkPkgs system;
|
||||
pkg = self.packages.${system}.default;
|
||||
logosCorePackage = logos-core.packages.${system}.default;
|
||||
logosExecutionZoneIndexerPackage = logos-execution-zone.packages.${system}.indexer;
|
||||
in
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
inputsFrom = [ pkg ];
|
||||
|
||||
inherit (pkg)
|
||||
LIBCLANG_PATH
|
||||
CLANG_PATH;
|
||||
|
||||
LOGOS_CORE_ROOT = "${logosCorePackage}";
|
||||
LOGOS_EXECUTION_ZONE_INDEXER_LIB = "${logosExecutionZoneIndexerPackage}/lib";
|
||||
LOGOS_EXECUTION_ZONE_INDEXER_INCLUDE = "${logosExecutionZoneIndexerPackage}/include";
|
||||
|
||||
shellHook = ''
|
||||
BLUE='\e[1;34m'
|
||||
GREEN='\e[1;32m'
|
||||
RESET='\e[0m'
|
||||
|
||||
echo -e "\n''${BLUE}=== Logos Execution Zone Module Development Environment ===''${RESET}"
|
||||
echo -e "''${GREEN}LOGOS_CORE_ROOT:''${RESET} $LOGOS_CORE_ROOT"
|
||||
echo -e "''${GREEN}LOGOS_EXECUTION_ZONE_INDEXER_LIB:''${RESET} $LOGOS_EXECUTION_ZONE_INDEXER_LIB"
|
||||
echo -e "''${GREEN}LOGOS_EXECUTION_ZONE_INDEXER_INCLUDE:''${RESET} $LOGOS_EXECUTION_ZONE_INDEXER_INCLUDE"
|
||||
echo -e "''${BLUE}---------------------------------------------------------''${RESET}"
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
25
justfile
25
justfile
@ -1,24 +1,23 @@
|
||||
default: build
|
||||
|
||||
configure:
|
||||
cmake -S . -B build -G Ninja \
|
||||
${LOGOS_CORE_ROOT:+-DLOGOS_CORE_ROOT="$LOGOS_CORE_ROOT"} \
|
||||
${LOGOS_EXECUTION_ZONE_INDEXER_LIB:+-DLOGOS_EXECUTION_ZONE_INDEXER_LIB="$LOGOS_EXECUTION_ZONE_INDEXER_LIB"} \
|
||||
${LOGOS_EXECUTION_ZONE_INDEXER_INCLUDE:+-DLOGOS_EXECUTION_ZONE_INDEXER_INCLUDE="$LOGOS_EXECUTION_ZONE_INDEXER_INCLUDE"}
|
||||
# Build the module plugin via logos-module-builder (-> result/lib/).
|
||||
build:
|
||||
nix build
|
||||
|
||||
build: configure
|
||||
cmake --build build --parallel --target lez_indexer_module
|
||||
# Drop into the builder dev shell.
|
||||
develop:
|
||||
nix develop
|
||||
|
||||
# Inspect the built plugin's methods + metadata.
|
||||
# lm result/lib/lez_indexer_module_plugin.so
|
||||
# Call a method (indexer must be started first):
|
||||
# logoscore -m result/lib -l lez_indexer_module -c "lez_indexer_module.getLastFinalizedBlockId()"
|
||||
|
||||
clean:
|
||||
rm -rf build result
|
||||
|
||||
rebuild: clean build
|
||||
|
||||
nix:
|
||||
nix develop
|
||||
|
||||
prettify:
|
||||
nix shell nixpkgs#clang-tools -c clang-format -i src/**.cpp src/**.h
|
||||
nix shell nixpkgs#clang-tools -c clang-format -i src/*.cpp src/*.h
|
||||
|
||||
unicode-logs file:
|
||||
perl -pe 's/\\u([0-9A-Fa-f]{4})/chr(hex($1))/ge' {{file}} | less -R
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
{
|
||||
"name": "lez_indexer_module",
|
||||
"version": "1.0.0",
|
||||
"description": "Logos Execution Zone Indexer Module for Logos Core",
|
||||
"author": "Logos Blockchain Team",
|
||||
"type": "core",
|
||||
"interface": "universal",
|
||||
"category": "blockchain",
|
||||
"main": "lez_indexer_module",
|
||||
"description": "Logos Execution Zone Indexer Module for Logos Core",
|
||||
"main": "lez_indexer_module_plugin",
|
||||
"dependencies": [],
|
||||
"capabilities": [],
|
||||
"include": ["libnomos.dylib"]
|
||||
}
|
||||
"nix": {
|
||||
"external_libraries": [{ "name": "indexer_ffi" }],
|
||||
"cmake": {
|
||||
"extra_include_dirs": ["lib"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <core/interface.h>
|
||||
|
||||
class ILezIndexerModule {
|
||||
public:
|
||||
virtual ~ILezIndexerModule() = default;
|
||||
|
||||
// === Logos Core ===
|
||||
|
||||
virtual void initLogos(LogosAPI* logosApiInstance) = 0;
|
||||
|
||||
// === Logos Execution Zone Indexer ===
|
||||
|
||||
// Indexer Lifecycle
|
||||
// `port` is taken as a QString (not uint16_t) because the Qt Remote Objects
|
||||
// ModuleProxy invokes by exact meta-type match and delivers caller arguments
|
||||
// as QString (the module-viewer reads every parameter as text); a uint16_t
|
||||
// parameter would never match. It is parsed to a port number internally.
|
||||
virtual int start_indexer(const QString& config_path, const QString& port) = 0;
|
||||
|
||||
// Indexer Queries
|
||||
//
|
||||
// Every parameter is a QString for the same QtRO exact-type-match reason as
|
||||
// `start_indexer` above: callers (consumer modules, the module-viewer) deliver
|
||||
// arguments as QString, so numeric/hash params are passed as text and parsed
|
||||
// internally. Each method returns a compact JSON string (the Logos protocol
|
||||
// data model is JSON-in-strings); an empty string means "not found" or a
|
||||
// failed query (the FFI does not log, so the body qWarns the status code).
|
||||
//
|
||||
// The JSON shape is tailored to what the LEZ explorer's Block/Transaction/
|
||||
// Account models consume (counts/sizes rather than raw arrays). Large 64-bit
|
||||
// values (ids, timestamps, validity windows) are emitted as decimal STRINGS
|
||||
// to avoid the double-precision loss of JSON numbers; counts/sizes are numbers.
|
||||
virtual QString getAccount(const QString& account_id) = 0;
|
||||
virtual QString getBlockById(const QString& block_id) = 0;
|
||||
virtual QString getBlockByHash(const QString& hash) = 0;
|
||||
virtual QString getTransaction(const QString& hash) = 0;
|
||||
// `before` is the optional pagination cursor: a block id to page back from,
|
||||
// or an empty string to start from the tip. `limit` caps the result count.
|
||||
virtual QString getBlocks(const QString& before, const QString& limit) = 0;
|
||||
virtual QString getLastFinalizedBlockId() = 0;
|
||||
// `offset` and `limit` are both required (the paging window).
|
||||
virtual QString getTransactionsByAccount(
|
||||
const QString& account_id,
|
||||
const QString& offset,
|
||||
const QString& limit
|
||||
) = 0;
|
||||
};
|
||||
|
||||
#define ILezIndexerModule_iid "org.logos.ilezindexermodule"
|
||||
Q_DECLARE_INTERFACE(ILezIndexerModule, ILezIndexerModule_iid)
|
||||
@ -1,28 +1,49 @@
|
||||
#include "lez_ffi_marshalling.h"
|
||||
|
||||
#include <QtCore/QByteArray>
|
||||
#include <QtCore/QJsonDocument>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace marshalling {
|
||||
|
||||
namespace {
|
||||
// Single hex nibble -> 0..15, or -1 if not a hex digit.
|
||||
int hexNibble(char c) {
|
||||
if (c >= '0' && c <= '9')
|
||||
return c - '0';
|
||||
if (c >= 'a' && c <= 'f')
|
||||
return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F')
|
||||
return c - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Lower-case hex of `length` raw bytes (used for 32-byte hashes/ids/keys and
|
||||
// 64-byte signatures). Qt's toHex() avoids a hand-rolled nibble loop.
|
||||
QString bytesToHex(const uint8_t* data, const size_t length) {
|
||||
return QString::fromLatin1(QByteArray(reinterpret_cast<const char*>(data), static_cast<int>(length)).toHex());
|
||||
// 64-byte signatures).
|
||||
std::string bytesToHex(const uint8_t* data, const size_t length) {
|
||||
static const char* digits = "0123456789abcdef";
|
||||
std::string out;
|
||||
out.resize(length * 2);
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
out[2 * i] = digits[(data[i] >> 4) & 0xF];
|
||||
out[2 * i + 1] = digits[data[i] & 0xF];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// FfiU128 is a 16-byte little-endian integer (balances, nonces). C++ has no
|
||||
// native u128, so build the decimal string via __uint128_t (GCC/Clang, 64-bit).
|
||||
QString u128LeToDecimal(const uint8_t data[16]) {
|
||||
std::string u128LeToDecimal(const uint8_t data[16]) {
|
||||
#if defined(__SIZEOF_INT128__) && __SIZEOF_INT128__ >= 16
|
||||
__uint128_t v = 0;
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
v |= static_cast<__uint128_t>(data[i]) << (i * 8);
|
||||
}
|
||||
if (v == 0) {
|
||||
return QStringLiteral("0");
|
||||
return "0";
|
||||
}
|
||||
char buf[40];
|
||||
int n = 0;
|
||||
@ -31,45 +52,58 @@ namespace marshalling {
|
||||
v /= 10;
|
||||
}
|
||||
std::reverse(buf, buf + n);
|
||||
return QString::fromLatin1(buf, n);
|
||||
return std::string(buf, n);
|
||||
#else
|
||||
#error "u128LeToDecimal requires __uint128_t; build with GCC or Clang on 64-bit"
|
||||
#endif
|
||||
}
|
||||
|
||||
// 64-bit values are emitted as decimal STRINGS, not JSON numbers: QJsonValue
|
||||
// stores numbers as double, which silently loses precision above 2^53.
|
||||
QString u64ToString(uint64_t v) {
|
||||
return QString::number(static_cast<qulonglong>(v));
|
||||
// 64-bit values are emitted as decimal STRINGS, not JSON numbers: JSON
|
||||
// numbers are doubles in many parsers and silently lose precision above 2^53.
|
||||
std::string u64ToString(uint64_t v) {
|
||||
return std::to_string(v);
|
||||
}
|
||||
|
||||
// Parse a hex string (optionally 0x-prefixed) into a fixed 32-byte FfiBytes32.
|
||||
// Returns false unless it decodes to exactly 32 bytes.
|
||||
bool hexToBytes32(const QString& hex, FfiBytes32* out) {
|
||||
QString trimmed = hex.trimmed();
|
||||
if (trimmed.startsWith(QLatin1String("0x")) || trimmed.startsWith(QLatin1String("0X"))) {
|
||||
trimmed = trimmed.mid(2);
|
||||
bool hexToBytes32(const std::string& hex, FfiBytes32* out) {
|
||||
size_t begin = 0;
|
||||
size_t end = hex.size();
|
||||
while (begin < end && std::isspace(static_cast<unsigned char>(hex[begin])))
|
||||
++begin;
|
||||
while (end > begin && std::isspace(static_cast<unsigned char>(hex[end - 1])))
|
||||
--end;
|
||||
|
||||
if (end - begin >= 2 && hex[begin] == '0' && (hex[begin + 1] == 'x' || hex[begin + 1] == 'X')) {
|
||||
begin += 2;
|
||||
}
|
||||
const QByteArray raw = QByteArray::fromHex(trimmed.toLatin1());
|
||||
if (raw.size() != 32) {
|
||||
|
||||
if (end - begin != 64) {
|
||||
return false;
|
||||
}
|
||||
std::memcpy(out->data, raw.constData(), 32);
|
||||
|
||||
for (size_t i = 0; i < 32; ++i) {
|
||||
const int hi = hexNibble(hex[begin + 2 * i]);
|
||||
const int lo = hexNibble(hex[begin + 2 * i + 1]);
|
||||
if (hi < 0 || lo < 0) {
|
||||
return false;
|
||||
}
|
||||
out->data[i] = static_cast<uint8_t>((hi << 4) | lo);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QJsonObject ffiAccountToJson(const FfiAccount& account) {
|
||||
QJsonObject obj;
|
||||
obj[QStringLiteral("program_owner")] =
|
||||
bytesToHex(reinterpret_cast<const uint8_t*>(account.program_owner.data), 32);
|
||||
obj[QStringLiteral("balance")] = u128LeToDecimal(account.balance.data);
|
||||
obj[QStringLiteral("nonce")] = u128LeToDecimal(account.nonce.data);
|
||||
obj[QStringLiteral("data_size")] = static_cast<int>(account.data_len);
|
||||
nlohmann::json ffiAccountToJson(const FfiAccount& account) {
|
||||
nlohmann::json obj;
|
||||
obj["program_owner"] = bytesToHex(reinterpret_cast<const uint8_t*>(account.program_owner.data), 32);
|
||||
obj["balance"] = u128LeToDecimal(account.balance.data);
|
||||
obj["nonce"] = u128LeToDecimal(account.nonce.data);
|
||||
obj["data_size"] = static_cast<int>(account.data_len);
|
||||
return obj;
|
||||
}
|
||||
|
||||
QJsonObject ffiTransactionToJson(const FfiTransaction& tx) {
|
||||
QJsonObject obj;
|
||||
nlohmann::json ffiTransactionToJson(const FfiTransaction& tx) {
|
||||
nlohmann::json obj;
|
||||
|
||||
switch (tx.kind) {
|
||||
case Public: {
|
||||
@ -77,30 +111,28 @@ namespace marshalling {
|
||||
if (!body) {
|
||||
break;
|
||||
}
|
||||
obj[QStringLiteral("type")] = QStringLiteral("Public");
|
||||
obj[QStringLiteral("hash")] = bytesToHex(body->hash.data, 32);
|
||||
obj[QStringLiteral("program_id")] =
|
||||
bytesToHex(reinterpret_cast<const uint8_t*>(body->message.program_id.data), 32);
|
||||
obj["type"] = "Public";
|
||||
obj["hash"] = bytesToHex(body->hash.data, 32);
|
||||
obj["program_id"] = bytesToHex(reinterpret_cast<const uint8_t*>(body->message.program_id.data), 32);
|
||||
|
||||
QJsonArray accounts;
|
||||
nlohmann::json accounts = nlohmann::json::array();
|
||||
const FfiAccountIdList& ids = body->message.account_ids;
|
||||
const FfiNonceList& nonces = body->message.nonces;
|
||||
for (uintptr_t i = 0; i < ids.len; ++i) {
|
||||
QJsonObject ref;
|
||||
ref[QStringLiteral("account_id")] = bytesToHex(ids.entries[i].data, 32);
|
||||
ref[QStringLiteral("nonce")] =
|
||||
i < nonces.len ? u128LeToDecimal(nonces.entries[i].data) : QStringLiteral("0");
|
||||
accounts.append(ref);
|
||||
nlohmann::json ref;
|
||||
ref["account_id"] = bytesToHex(ids.entries[i].data, 32);
|
||||
ref["nonce"] = i < nonces.len ? u128LeToDecimal(nonces.entries[i].data) : std::string("0");
|
||||
accounts.push_back(ref);
|
||||
}
|
||||
obj[QStringLiteral("accounts")] = accounts;
|
||||
obj["accounts"] = accounts;
|
||||
|
||||
QJsonArray instructionData;
|
||||
nlohmann::json instructionData = nlohmann::json::array();
|
||||
const FfiInstructionDataList& instr = body->message.instruction_data;
|
||||
for (uintptr_t i = 0; i < instr.len; ++i) {
|
||||
instructionData.append(static_cast<double>(instr.entries[i]));
|
||||
instructionData.push_back(static_cast<std::int64_t>(instr.entries[i]));
|
||||
}
|
||||
obj[QStringLiteral("instruction_data")] = instructionData;
|
||||
obj[QStringLiteral("signature_count")] = static_cast<int>(body->witness_set.len);
|
||||
obj["instruction_data"] = instructionData;
|
||||
obj["signature_count"] = static_cast<int>(body->witness_set.len);
|
||||
break;
|
||||
}
|
||||
case Private: {
|
||||
@ -108,29 +140,27 @@ namespace marshalling {
|
||||
if (!body) {
|
||||
break;
|
||||
}
|
||||
obj[QStringLiteral("type")] = QStringLiteral("PrivacyPreserving");
|
||||
obj[QStringLiteral("hash")] = bytesToHex(body->hash.data, 32);
|
||||
obj["type"] = "PrivacyPreserving";
|
||||
obj["hash"] = bytesToHex(body->hash.data, 32);
|
||||
|
||||
QJsonArray accounts;
|
||||
nlohmann::json accounts = nlohmann::json::array();
|
||||
const FfiAccountIdList& ids = body->message.public_account_ids;
|
||||
const FfiNonceList& nonces = body->message.nonces;
|
||||
for (uintptr_t i = 0; i < ids.len; ++i) {
|
||||
QJsonObject ref;
|
||||
ref[QStringLiteral("account_id")] = bytesToHex(ids.entries[i].data, 32);
|
||||
ref[QStringLiteral("nonce")] =
|
||||
i < nonces.len ? u128LeToDecimal(nonces.entries[i].data) : QStringLiteral("0");
|
||||
accounts.append(ref);
|
||||
nlohmann::json ref;
|
||||
ref["account_id"] = bytesToHex(ids.entries[i].data, 32);
|
||||
ref["nonce"] = i < nonces.len ? u128LeToDecimal(nonces.entries[i].data) : std::string("0");
|
||||
accounts.push_back(ref);
|
||||
}
|
||||
obj[QStringLiteral("accounts")] = accounts;
|
||||
obj["accounts"] = accounts;
|
||||
|
||||
obj[QStringLiteral("new_commitments_count")] = static_cast<int>(body->message.new_commitments.len);
|
||||
obj[QStringLiteral("nullifiers_count")] = static_cast<int>(body->message.new_nullifiers.len);
|
||||
obj[QStringLiteral("encrypted_states_count")] =
|
||||
static_cast<int>(body->message.encrypted_private_post_states.len);
|
||||
obj[QStringLiteral("validity_window_start")] = u64ToString(body->message.block_validity_window[0]);
|
||||
obj[QStringLiteral("validity_window_end")] = u64ToString(body->message.block_validity_window[1]);
|
||||
obj[QStringLiteral("signature_count")] = static_cast<int>(body->witness_set.len);
|
||||
obj[QStringLiteral("proof_size")] = static_cast<int>(body->proof.len);
|
||||
obj["new_commitments_count"] = static_cast<int>(body->message.new_commitments.len);
|
||||
obj["nullifiers_count"] = static_cast<int>(body->message.new_nullifiers.len);
|
||||
obj["encrypted_states_count"] = static_cast<int>(body->message.encrypted_private_post_states.len);
|
||||
obj["validity_window_start"] = u64ToString(body->message.block_validity_window[0]);
|
||||
obj["validity_window_end"] = u64ToString(body->message.block_validity_window[1]);
|
||||
obj["signature_count"] = static_cast<int>(body->witness_set.len);
|
||||
obj["proof_size"] = static_cast<int>(body->proof.len);
|
||||
break;
|
||||
}
|
||||
case ProgramDeploy: {
|
||||
@ -138,9 +168,9 @@ namespace marshalling {
|
||||
if (!body) {
|
||||
break;
|
||||
}
|
||||
obj[QStringLiteral("type")] = QStringLiteral("ProgramDeployment");
|
||||
obj[QStringLiteral("hash")] = bytesToHex(body->hash.data, 32);
|
||||
obj[QStringLiteral("bytecode_size")] = static_cast<int>(body->message.len);
|
||||
obj["type"] = "ProgramDeployment";
|
||||
obj["hash"] = bytesToHex(body->hash.data, 32);
|
||||
obj["bytecode_size"] = static_cast<int>(body->message.len);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -149,42 +179,38 @@ namespace marshalling {
|
||||
}
|
||||
|
||||
namespace {
|
||||
QString bedrockStatusToString(FfiBedrockStatus status) {
|
||||
std::string bedrockStatusToString(FfiBedrockStatus status) {
|
||||
switch (status) {
|
||||
case Safe:
|
||||
return QStringLiteral("Safe");
|
||||
return "Safe";
|
||||
case Finalized:
|
||||
return QStringLiteral("Finalized");
|
||||
return "Finalized";
|
||||
case Pending:
|
||||
default:
|
||||
return QStringLiteral("Pending");
|
||||
return "Pending";
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
QJsonObject ffiBlockToJson(const FfiBlock& block) {
|
||||
QJsonObject obj;
|
||||
obj[QStringLiteral("block_id")] = u64ToString(block.header.block_id);
|
||||
obj[QStringLiteral("hash")] = bytesToHex(block.header.hash.data, 32);
|
||||
obj[QStringLiteral("prev_block_hash")] = bytesToHex(block.header.prev_block_hash.data, 32);
|
||||
obj[QStringLiteral("timestamp")] = u64ToString(block.header.timestamp);
|
||||
obj[QStringLiteral("signature")] = bytesToHex(block.header.signature.data, 64);
|
||||
obj[QStringLiteral("bedrock_status")] = bedrockStatusToString(block.bedrock_status);
|
||||
nlohmann::json ffiBlockToJson(const FfiBlock& block) {
|
||||
nlohmann::json obj;
|
||||
obj["block_id"] = u64ToString(block.header.block_id);
|
||||
obj["hash"] = bytesToHex(block.header.hash.data, 32);
|
||||
obj["prev_block_hash"] = bytesToHex(block.header.prev_block_hash.data, 32);
|
||||
obj["timestamp"] = u64ToString(block.header.timestamp);
|
||||
obj["signature"] = bytesToHex(block.header.signature.data, 64);
|
||||
obj["bedrock_status"] = bedrockStatusToString(block.bedrock_status);
|
||||
|
||||
QJsonArray transactions;
|
||||
nlohmann::json transactions = nlohmann::json::array();
|
||||
for (uintptr_t i = 0; i < block.body.len; ++i) {
|
||||
transactions.append(ffiTransactionToJson(block.body.entries[i]));
|
||||
transactions.push_back(ffiTransactionToJson(block.body.entries[i]));
|
||||
}
|
||||
obj[QStringLiteral("transactions")] = transactions;
|
||||
obj["transactions"] = transactions;
|
||||
return obj;
|
||||
}
|
||||
|
||||
QString jsonToCompactString(const QJsonObject& obj) {
|
||||
return QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
|
||||
QString jsonToCompactString(const QJsonArray& arr) {
|
||||
return QString::fromUtf8(QJsonDocument(arr).toJson(QJsonDocument::Compact));
|
||||
std::string jsonToCompactString(const nlohmann::json& j) {
|
||||
return j.dump();
|
||||
}
|
||||
|
||||
} // namespace marshalling
|
||||
|
||||
@ -1,19 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "lez_indexer_ffi.h"
|
||||
#include <indexer_ffi.h>
|
||||
|
||||
#include <QtCore/QJsonArray>
|
||||
#include <QtCore/QJsonObject>
|
||||
#include <QtCore/QString>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
// FFI marshalling helpers
|
||||
//
|
||||
// Bridge the raw C structs returned by the indexer FFI (indexer_ffi.h) to the
|
||||
// compact JSON the LEZ explorer's Block/Transaction/Account models consume, and
|
||||
// parse the QString inputs back into FFI types. Kept in their own translation
|
||||
// unit so lez_indexer_module.cpp stays focused on the query flow and the JSON
|
||||
// data model lives in one place — which is also what makes it easy to share
|
||||
// with other modules later.
|
||||
// parse the std::string inputs back into FFI types. Kept in their own
|
||||
// translation unit so lez_indexer_module_impl.cpp stays focused on the query
|
||||
// flow and the JSON data model lives in one place.
|
||||
//
|
||||
// This header pulls in the FFI types + nlohmann::json, so it is included only
|
||||
// from .cpp files — NOT from lez_indexer_module_impl.h, whose Qt-free,
|
||||
// FFI-free shape the universal codegen parses.
|
||||
//
|
||||
// These never take ownership of FFI memory: the caller frees it with the
|
||||
// matching free_ffi_* function AFTER marshalling.
|
||||
@ -25,24 +27,24 @@ namespace marshalling {
|
||||
|
||||
// Lower-case hex of `length` raw bytes (32-byte hashes/ids/keys, 64-byte
|
||||
// signatures).
|
||||
QString bytesToHex(const uint8_t* data, size_t length);
|
||||
std::string bytesToHex(const uint8_t* data, size_t length);
|
||||
|
||||
// FfiU128 is a 16-byte little-endian integer (balances, nonces); rendered
|
||||
// as a decimal string.
|
||||
QString u128LeToDecimal(const uint8_t data[16]);
|
||||
std::string u128LeToDecimal(const uint8_t data[16]);
|
||||
|
||||
// 64-bit value as a decimal string.
|
||||
QString u64ToString(uint64_t v);
|
||||
std::string u64ToString(uint64_t v);
|
||||
|
||||
// Parse a hex string (optionally 0x-prefixed) into a fixed 32-byte buffer.
|
||||
// Returns false unless it decodes to exactly 32 bytes.
|
||||
bool hexToBytes32(const QString& hex, FfiBytes32* out);
|
||||
bool hexToBytes32(const std::string& hex, FfiBytes32* out);
|
||||
|
||||
QJsonObject ffiAccountToJson(const FfiAccount& account);
|
||||
QJsonObject ffiTransactionToJson(const FfiTransaction& tx);
|
||||
QJsonObject ffiBlockToJson(const FfiBlock& block);
|
||||
nlohmann::json ffiAccountToJson(const FfiAccount& account);
|
||||
nlohmann::json ffiTransactionToJson(const FfiTransaction& tx);
|
||||
nlohmann::json ffiBlockToJson(const FfiBlock& block);
|
||||
|
||||
QString jsonToCompactString(const QJsonObject& obj);
|
||||
QString jsonToCompactString(const QJsonArray& arr);
|
||||
// Compact serialization (no spaces/newlines) of an object or array.
|
||||
std::string jsonToCompactString(const nlohmann::json& j);
|
||||
|
||||
} // namespace marshalling
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
// The generated indexer_ffi.h carries no include guard, so including it from
|
||||
// more than one header in the same translation unit redefines every type.
|
||||
// Wrap it once here (behind #pragma once) and include THIS header instead of
|
||||
// <indexer_ffi.h> directly, so the FFI types can be shared by both
|
||||
// lez_indexer_module.h and lez_ffi_marshalling.h.
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
#include <indexer_ffi.h>
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -1,288 +0,0 @@
|
||||
#include "lez_indexer_module.h"
|
||||
|
||||
#include "lez_ffi_marshalling.h"
|
||||
|
||||
#include <QtCore/QByteArray>
|
||||
#include <QtCore/QDebug>
|
||||
#include <QtCore/QJsonArray>
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QVariantMap>
|
||||
|
||||
using namespace marshalling;
|
||||
|
||||
LezIndexerModule::LezIndexerModule() = default;
|
||||
|
||||
LezIndexerModule::~LezIndexerModule() {
|
||||
if (indexer_service_ffi) {
|
||||
OperationStatus operation_result = stop_indexer(indexer_service_ffi);
|
||||
|
||||
if (is_error(&operation_result)) {
|
||||
int signal = operation_result;
|
||||
qWarning() << "destructor: indexer FFI error: " << signal;
|
||||
}
|
||||
|
||||
indexer_service_ffi = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// === Plugin Interface ===
|
||||
|
||||
QString LezIndexerModule::name() const {
|
||||
return "lez_indexer_module";
|
||||
}
|
||||
|
||||
QString LezIndexerModule::version() const {
|
||||
return "1.0.0";
|
||||
}
|
||||
|
||||
// === Logos Core ===
|
||||
|
||||
void LezIndexerModule::initLogos(LogosAPI* logosApiInstance) {
|
||||
logosAPI = logosApiInstance;
|
||||
}
|
||||
|
||||
// === Indexer Lifecycle ===
|
||||
|
||||
int LezIndexerModule::start_indexer(const QString& config_path, const QString& port) {
|
||||
if (!indexer_service_ffi) {
|
||||
bool ok = false;
|
||||
const uint16_t port_num = port.toUShort(&ok);
|
||||
if (!ok) {
|
||||
qWarning() << "start_indexer: invalid port:" << port;
|
||||
return -1;
|
||||
}
|
||||
|
||||
QByteArray utf8 = config_path.toUtf8();
|
||||
const char* c_path = utf8.constData();
|
||||
|
||||
InitializedIndexerServiceFFIResult indexer_service_ffi_res = ::start_indexer(c_path, port_num);
|
||||
|
||||
if (is_error(&indexer_service_ffi_res.error)) {
|
||||
int signal = indexer_service_ffi_res.error;
|
||||
qWarning() << "start_indexer: indexer FFI error: " << signal;
|
||||
|
||||
return signal;
|
||||
}
|
||||
|
||||
indexer_service_ffi = indexer_service_ffi_res.value;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// === Indexer Queries ===
|
||||
//
|
||||
// Each method calls the matching query_* FFI function on the handle we already
|
||||
// hold (no Runtime needed — the pinned FFI carries its own runtime inside the
|
||||
// handle), marshals the returned C struct into compact JSON, then frees the
|
||||
// FFI's deep allocations with the matching free_ffi_* function.
|
||||
//
|
||||
// Known leak: query_* returns its payload in a `Box::into_raw` *outer* box
|
||||
// (PointerResult.value), and the free_ffi_* functions free only the inner
|
||||
// boxes/vectors (they take the struct by value). The pinned FFI provides no
|
||||
// free for the outer box, and libc free() would be UB if Rust's global
|
||||
// allocator differs from the system one, so we deliberately leak the small
|
||||
// (~8-32 byte) outer wrapper per query. Tracked for an upstream fix (add a
|
||||
// free_*_result, or switch the query API to out-params).
|
||||
|
||||
QString LezIndexerModule::getAccount(const QString& account_id) {
|
||||
if (!indexer_service_ffi) {
|
||||
qWarning() << "getAccount: indexer not started";
|
||||
return {};
|
||||
}
|
||||
|
||||
FfiAccountId id;
|
||||
if (!hexToBytes32(account_id, &id)) {
|
||||
qWarning() << "getAccount: invalid account id (need 32-byte hex):" << account_id;
|
||||
return {};
|
||||
}
|
||||
|
||||
PointerResult_FfiAccount__OperationStatus res = ::query_account(indexer_service_ffi, id);
|
||||
if (is_error(&res.error) || !res.value) {
|
||||
qWarning() << "getAccount: indexer FFI error:" << res.error;
|
||||
return {};
|
||||
}
|
||||
|
||||
const QString out = jsonToCompactString(ffiAccountToJson(*res.value));
|
||||
::free_ffi_account(*res.value);
|
||||
return out;
|
||||
}
|
||||
|
||||
QString LezIndexerModule::getBlockById(const QString& block_id) {
|
||||
if (!indexer_service_ffi) {
|
||||
qWarning() << "getBlockById: indexer not started";
|
||||
return {};
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
const uint64_t id = block_id.toULongLong(&ok);
|
||||
if (!ok) {
|
||||
qWarning() << "getBlockById: invalid block id:" << block_id;
|
||||
return {};
|
||||
}
|
||||
|
||||
PointerResult_FfiBlockOpt__OperationStatus res = ::query_block(indexer_service_ffi, id);
|
||||
if (is_error(&res.error) || !res.value) {
|
||||
qWarning() << "getBlockById: indexer FFI error:" << res.error;
|
||||
return {};
|
||||
}
|
||||
|
||||
QString out;
|
||||
if (res.value->is_some && res.value->value) {
|
||||
out = jsonToCompactString(ffiBlockToJson(*res.value->value));
|
||||
}
|
||||
::free_ffi_block_opt(*res.value);
|
||||
return out;
|
||||
}
|
||||
|
||||
QString LezIndexerModule::getBlockByHash(const QString& hash) {
|
||||
if (!indexer_service_ffi) {
|
||||
qWarning() << "getBlockByHash: indexer not started";
|
||||
return {};
|
||||
}
|
||||
|
||||
FfiHashType h;
|
||||
if (!hexToBytes32(hash, &h)) {
|
||||
qWarning() << "getBlockByHash: invalid hash (need 32-byte hex):" << hash;
|
||||
return {};
|
||||
}
|
||||
|
||||
PointerResult_FfiBlockOpt__OperationStatus res = ::query_block_by_hash(indexer_service_ffi, h);
|
||||
if (is_error(&res.error) || !res.value) {
|
||||
qWarning() << "getBlockByHash: indexer FFI error:" << res.error;
|
||||
return {};
|
||||
}
|
||||
|
||||
QString out;
|
||||
if (res.value->is_some && res.value->value) {
|
||||
out = jsonToCompactString(ffiBlockToJson(*res.value->value));
|
||||
}
|
||||
::free_ffi_block_opt(*res.value);
|
||||
return out;
|
||||
}
|
||||
|
||||
QString LezIndexerModule::getTransaction(const QString& hash) {
|
||||
if (!indexer_service_ffi) {
|
||||
qWarning() << "getTransaction: indexer not started";
|
||||
return {};
|
||||
}
|
||||
|
||||
FfiHashType h;
|
||||
if (!hexToBytes32(hash, &h)) {
|
||||
qWarning() << "getTransaction: invalid hash (need 32-byte hex):" << hash;
|
||||
return {};
|
||||
}
|
||||
|
||||
PointerResult_FfiOption_FfiTransaction_____OperationStatus res = ::query_transaction(indexer_service_ffi, h);
|
||||
if (is_error(&res.error) || !res.value) {
|
||||
qWarning() << "getTransaction: indexer FFI error:" << res.error;
|
||||
return {};
|
||||
}
|
||||
|
||||
QString out;
|
||||
if (res.value->is_some && res.value->value) {
|
||||
out = jsonToCompactString(ffiTransactionToJson(*res.value->value));
|
||||
}
|
||||
::free_ffi_transaction_opt(*res.value);
|
||||
return out;
|
||||
}
|
||||
|
||||
QString LezIndexerModule::getBlocks(const QString& before, const QString& limit) {
|
||||
if (!indexer_service_ffi) {
|
||||
qWarning() << "getBlocks: indexer not started";
|
||||
return {};
|
||||
}
|
||||
|
||||
bool limitOk = false;
|
||||
const uint64_t limitNum = limit.toULongLong(&limitOk);
|
||||
if (!limitOk) {
|
||||
qWarning() << "getBlocks: invalid limit:" << limit;
|
||||
return {};
|
||||
}
|
||||
|
||||
// `before` is the optional pagination cursor: an empty string means "from
|
||||
// the tip", a non-empty value that fails to parse is an error. `beforeVal`
|
||||
// must outlive the call since FfiOption_u64 borrows its address.
|
||||
uint64_t beforeVal = 0;
|
||||
bool hasBefore = false;
|
||||
if (!before.isEmpty()) {
|
||||
beforeVal = before.toULongLong(&hasBefore);
|
||||
if (!hasBefore) {
|
||||
qWarning() << "getBlocks: invalid before:" << before;
|
||||
return {};
|
||||
}
|
||||
}
|
||||
FfiOption_u64 beforeOpt;
|
||||
beforeOpt.is_some = hasBefore;
|
||||
beforeOpt.value = hasBefore ? &beforeVal : nullptr;
|
||||
|
||||
PointerResult_FfiVec_FfiBlock_____OperationStatus res = ::query_block_vec(indexer_service_ffi, beforeOpt, limitNum);
|
||||
if (is_error(&res.error) || !res.value) {
|
||||
qWarning() << "getBlocks: indexer FFI error:" << res.error;
|
||||
return {};
|
||||
}
|
||||
|
||||
QJsonArray arr;
|
||||
for (uintptr_t i = 0; i < res.value->len; ++i) {
|
||||
arr.append(ffiBlockToJson(res.value->entries[i]));
|
||||
}
|
||||
::free_ffi_block_vec(*res.value);
|
||||
return jsonToCompactString(arr);
|
||||
}
|
||||
|
||||
QString LezIndexerModule::getLastFinalizedBlockId() {
|
||||
if (!indexer_service_ffi) {
|
||||
qWarning() << "getLastFinalizedBlockId: indexer not started";
|
||||
return {};
|
||||
}
|
||||
|
||||
PointerResult_u64__OperationStatus res = ::query_last_block(indexer_service_ffi);
|
||||
if (is_error(&res.error) || !res.value) {
|
||||
qWarning() << "getLastFinalizedBlockId: indexer FFI error:" << res.error;
|
||||
return {};
|
||||
}
|
||||
|
||||
// Bare decimal string; the FFI provides no free for the boxed u64 (leaked,
|
||||
// see the note above).
|
||||
return u64ToString(*res.value);
|
||||
}
|
||||
|
||||
QString LezIndexerModule::getTransactionsByAccount(
|
||||
const QString& account_id,
|
||||
const QString& offset,
|
||||
const QString& limit
|
||||
) {
|
||||
if (!indexer_service_ffi) {
|
||||
qWarning() << "getTransactionsByAccount: indexer not started";
|
||||
return {};
|
||||
}
|
||||
|
||||
FfiAccountId id;
|
||||
if (!hexToBytes32(account_id, &id)) {
|
||||
qWarning() << "getTransactionsByAccount: invalid account id (need 32-byte hex):" << account_id;
|
||||
return {};
|
||||
}
|
||||
|
||||
bool offsetOk = false;
|
||||
bool limitOk = false;
|
||||
const uint64_t offsetNum = offset.toULongLong(&offsetOk);
|
||||
const uint64_t limitNum = limit.toULongLong(&limitOk);
|
||||
if (!offsetOk || !limitOk) {
|
||||
qWarning() << "getTransactionsByAccount: invalid offset/limit:" << offset << limit;
|
||||
return {};
|
||||
}
|
||||
|
||||
PointerResult_FfiVec_FfiTransaction_____OperationStatus res =
|
||||
::query_transactions_by_account(indexer_service_ffi, id, offsetNum, limitNum);
|
||||
if (is_error(&res.error) || !res.value) {
|
||||
qWarning() << "getTransactionsByAccount: indexer FFI error:" << res.error;
|
||||
return {};
|
||||
}
|
||||
|
||||
QJsonArray arr;
|
||||
for (uintptr_t i = 0; i < res.value->len; ++i) {
|
||||
arr.append(ffiTransactionToJson(res.value->entries[i]));
|
||||
}
|
||||
::free_ffi_transaction_vec(*res.value);
|
||||
return jsonToCompactString(arr);
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "i_lez_indexer_module.h"
|
||||
#include "lez_indexer_ffi.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QVariantList>
|
||||
|
||||
class LezIndexerModule : public QObject, public PluginInterface, public ILezIndexerModule {
|
||||
Q_OBJECT
|
||||
Q_PLUGIN_METADATA(IID ILezIndexerModule_iid FILE LEZ_INDEXER_MODULE_METADATA_FILE)
|
||||
Q_INTERFACES(PluginInterface ILezIndexerModule)
|
||||
|
||||
private:
|
||||
LogosAPI* logosApi = nullptr;
|
||||
IndexerServiceFFI* indexer_service_ffi = nullptr;
|
||||
|
||||
public:
|
||||
LezIndexerModule();
|
||||
~LezIndexerModule() override;
|
||||
|
||||
// === Plugin Interface ===
|
||||
[[nodiscard]] QString name() const override;
|
||||
[[nodiscard]] QString version() const override;
|
||||
|
||||
// === Logos Core ===
|
||||
|
||||
Q_INVOKABLE void initLogos(LogosAPI* logosApiInstance) override;
|
||||
|
||||
// === Logos Execution Zone Indexer ===
|
||||
|
||||
// Indexer Lifecycle
|
||||
Q_INVOKABLE int start_indexer(const QString& config_path, const QString& port) override;
|
||||
|
||||
// Indexer Queries (return compact JSON strings; see i_lez_indexer_module.h)
|
||||
Q_INVOKABLE QString getAccount(const QString& account_id) override;
|
||||
Q_INVOKABLE QString getBlockById(const QString& block_id) override;
|
||||
Q_INVOKABLE QString getBlockByHash(const QString& hash) override;
|
||||
Q_INVOKABLE QString getTransaction(const QString& hash) override;
|
||||
Q_INVOKABLE QString getBlocks(const QString& before, const QString& limit) override;
|
||||
Q_INVOKABLE QString getLastFinalizedBlockId() override;
|
||||
Q_INVOKABLE QString
|
||||
getTransactionsByAccount(const QString& account_id, const QString& offset, const QString& limit) override;
|
||||
|
||||
// Indexer Logging (opt-in)
|
||||
//
|
||||
// Installs the indexer FFI's logger (env_logger) so the indexer's Rust `log`
|
||||
// output surfaces in the host process.
|
||||
//
|
||||
// Commented out because the underlying `::init_logger()` export does not yet
|
||||
// exist in the pinned logos-execution-zone FFI
|
||||
//
|
||||
// Uncomment this single block once the export lands upstream
|
||||
// and the pin is bumped to a rev that includes it.
|
||||
//
|
||||
// Q_INVOKABLE void init_logger() { ::init_logger(); }
|
||||
|
||||
signals:
|
||||
void eventResponse(const QString& eventName, const QVariantList& data);
|
||||
};
|
||||
297
src/lez_indexer_module_impl.cpp
Normal file
297
src/lez_indexer_module_impl.cpp
Normal file
@ -0,0 +1,297 @@
|
||||
#include "lez_indexer_module_impl.h"
|
||||
|
||||
#include "lez_ffi_marshalling.h"
|
||||
#include <indexer_ffi.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
using namespace marshalling;
|
||||
|
||||
namespace {
|
||||
// The impl header keeps the handle opaque (void*) so the universal codegen
|
||||
// never needs the FFI types; recover the real type here.
|
||||
inline IndexerServiceFFI* handle(void* p) {
|
||||
return static_cast<IndexerServiceFFI*>(p);
|
||||
}
|
||||
|
||||
void warn(const char* method, const char* msg) {
|
||||
std::fprintf(stderr, "lez_indexer_module: %s: %s\n", method, msg);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
LezIndexerModuleImpl::~LezIndexerModuleImpl() {
|
||||
if (stop_indexer() != 0) {
|
||||
warn("destructor", "indexer FFI error on stop");
|
||||
}
|
||||
}
|
||||
|
||||
// === Indexer Lifecycle ===
|
||||
|
||||
int64_t LezIndexerModuleImpl::start_indexer(const std::string& config_path) {
|
||||
if (!indexer_service_ffi) {
|
||||
// Null runtime: the FFI creates and owns its own tokio runtime. Storage
|
||||
// goes under this module's instance persistence path (host-owned, stable
|
||||
// per Basecamp --user-dir) so RocksDB never lands in the process CWD.
|
||||
InitializedIndexerServiceFFIResult res =
|
||||
::start_indexer(nullptr, config_path.c_str(), instancePersistencePath().c_str());
|
||||
if (is_error(&res.error)) {
|
||||
warn("start_indexer", "indexer FFI error");
|
||||
return static_cast<int64_t>(res.error);
|
||||
}
|
||||
|
||||
indexer_service_ffi = res.value;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int64_t LezIndexerModuleImpl::stop_indexer() {
|
||||
if (!indexer_service_ffi) {
|
||||
return 0; // not running
|
||||
}
|
||||
// stop_indexer frees the handle; null ours before returning so a later start
|
||||
// (or the destructor) doesn't double-free or operate on a dead pointer.
|
||||
OperationStatus operation_result = ::stop_indexer(handle(indexer_service_ffi));
|
||||
indexer_service_ffi = nullptr;
|
||||
if (is_error(&operation_result)) {
|
||||
warn("stop_indexer", "indexer FFI error on stop");
|
||||
return static_cast<int64_t>(operation_result);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void LezIndexerModuleImpl::init_logger(const std::string& level) {
|
||||
::init_logger(level.c_str());
|
||||
}
|
||||
|
||||
// === Indexer Queries ===
|
||||
//
|
||||
// Each method calls the matching query_* FFI function on the handle we hold,
|
||||
// marshals the returned C struct into compact JSON, then frees the FFI
|
||||
// allocation with the matching free_ffi_* function. The frees take the outer
|
||||
// pointer (PointerResult.value) and reclaim the whole allocation.
|
||||
|
||||
std::string LezIndexerModuleImpl::getAccount(const std::string& account_id) {
|
||||
if (!indexer_service_ffi) {
|
||||
warn("getAccount", "indexer not started");
|
||||
return {};
|
||||
}
|
||||
|
||||
FfiAccountId id;
|
||||
if (!hexToBytes32(account_id, &id)) {
|
||||
warn("getAccount", "invalid account id (need 32-byte hex)");
|
||||
return {};
|
||||
}
|
||||
|
||||
PointerResult_FfiAccount__OperationStatus res = ::query_account(handle(indexer_service_ffi), id);
|
||||
if (is_error(&res.error) || !res.value) {
|
||||
warn("getAccount", "indexer FFI error");
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::string out = jsonToCompactString(ffiAccountToJson(*res.value));
|
||||
::free_ffi_account(res.value);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string LezIndexerModuleImpl::getBlockById(const std::string& block_id) {
|
||||
if (!indexer_service_ffi) {
|
||||
warn("getBlockById", "indexer not started");
|
||||
return {};
|
||||
}
|
||||
|
||||
char* end = nullptr;
|
||||
const uint64_t id = std::strtoull(block_id.c_str(), &end, 10);
|
||||
if (end == block_id.c_str() || *end != '\0') {
|
||||
warn("getBlockById", "invalid block id");
|
||||
return {};
|
||||
}
|
||||
|
||||
PointerResult_FfiBlockOpt__OperationStatus res = ::query_block(handle(indexer_service_ffi), id);
|
||||
if (is_error(&res.error) || !res.value) {
|
||||
warn("getBlockById", "indexer FFI error");
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string out;
|
||||
if (res.value->is_some && res.value->value) {
|
||||
out = jsonToCompactString(ffiBlockToJson(*res.value->value));
|
||||
}
|
||||
::free_ffi_block_opt(res.value);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string LezIndexerModuleImpl::getBlockByHash(const std::string& hash) {
|
||||
if (!indexer_service_ffi) {
|
||||
warn("getBlockByHash", "indexer not started");
|
||||
return {};
|
||||
}
|
||||
|
||||
FfiHashType h;
|
||||
if (!hexToBytes32(hash, &h)) {
|
||||
warn("getBlockByHash", "invalid hash (need 32-byte hex)");
|
||||
return {};
|
||||
}
|
||||
|
||||
PointerResult_FfiBlockOpt__OperationStatus res = ::query_block_by_hash(handle(indexer_service_ffi), h);
|
||||
if (is_error(&res.error) || !res.value) {
|
||||
warn("getBlockByHash", "indexer FFI error");
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string out;
|
||||
if (res.value->is_some && res.value->value) {
|
||||
out = jsonToCompactString(ffiBlockToJson(*res.value->value));
|
||||
}
|
||||
::free_ffi_block_opt(res.value);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string LezIndexerModuleImpl::getTransaction(const std::string& hash) {
|
||||
if (!indexer_service_ffi) {
|
||||
warn("getTransaction", "indexer not started");
|
||||
return {};
|
||||
}
|
||||
|
||||
FfiHashType h;
|
||||
if (!hexToBytes32(hash, &h)) {
|
||||
warn("getTransaction", "invalid hash (need 32-byte hex)");
|
||||
return {};
|
||||
}
|
||||
|
||||
PointerResult_FfiOption_FfiTransaction_____OperationStatus res =
|
||||
::query_transaction(handle(indexer_service_ffi), h);
|
||||
if (is_error(&res.error) || !res.value) {
|
||||
warn("getTransaction", "indexer FFI error");
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string out;
|
||||
if (res.value->is_some && res.value->value) {
|
||||
out = jsonToCompactString(ffiTransactionToJson(*res.value->value));
|
||||
}
|
||||
::free_ffi_transaction_opt(res.value);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string LezIndexerModuleImpl::getBlocks(const std::string& before, const std::string& limit) {
|
||||
if (!indexer_service_ffi) {
|
||||
warn("getBlocks", "indexer not started");
|
||||
return {};
|
||||
}
|
||||
|
||||
char* end = nullptr;
|
||||
const uint64_t limitNum = std::strtoull(limit.c_str(), &end, 10);
|
||||
if (end == limit.c_str() || *end != '\0') {
|
||||
warn("getBlocks", "invalid limit");
|
||||
return {};
|
||||
}
|
||||
|
||||
// `before` is the optional pagination cursor: an empty string means "from
|
||||
// the tip", a non-empty value that fails to parse is an error. `beforeVal`
|
||||
// must outlive the call since FfiOption_u64 borrows its address.
|
||||
uint64_t beforeVal = 0;
|
||||
bool hasBefore = false;
|
||||
if (!before.empty()) {
|
||||
char* bend = nullptr;
|
||||
beforeVal = std::strtoull(before.c_str(), &bend, 10);
|
||||
if (bend == before.c_str() || *bend != '\0') {
|
||||
warn("getBlocks", "invalid before");
|
||||
return {};
|
||||
}
|
||||
hasBefore = true;
|
||||
}
|
||||
FfiOption_u64 beforeOpt;
|
||||
beforeOpt.is_some = hasBefore;
|
||||
beforeOpt.value = hasBefore ? &beforeVal : nullptr;
|
||||
|
||||
PointerResult_FfiVec_FfiBlock_____OperationStatus res =
|
||||
::query_block_vec(handle(indexer_service_ffi), beforeOpt, limitNum);
|
||||
if (is_error(&res.error) || !res.value) {
|
||||
warn("getBlocks", "indexer FFI error");
|
||||
return {};
|
||||
}
|
||||
|
||||
nlohmann::json arr = nlohmann::json::array();
|
||||
for (uintptr_t i = 0; i < res.value->len; ++i) {
|
||||
arr.push_back(ffiBlockToJson(res.value->entries[i]));
|
||||
}
|
||||
::free_ffi_block_vec(res.value);
|
||||
return jsonToCompactString(arr);
|
||||
}
|
||||
|
||||
std::string LezIndexerModuleImpl::getLastFinalizedBlockId() {
|
||||
if (!indexer_service_ffi) {
|
||||
warn("getLastFinalizedBlockId", "indexer not started");
|
||||
return {};
|
||||
}
|
||||
|
||||
LastBlockIdResult res = ::query_last_block(handle(indexer_service_ffi));
|
||||
if (is_error(&res.error) || !res.is_some) {
|
||||
warn("getLastFinalizedBlockId", "indexer FFI error or no finalized block yet");
|
||||
return {};
|
||||
}
|
||||
|
||||
// Bare decimal string; the id is returned inline, nothing to free.
|
||||
return u64ToString(res.block_id);
|
||||
}
|
||||
|
||||
std::string LezIndexerModuleImpl::getStatus() {
|
||||
if (!indexer_service_ffi) {
|
||||
warn("getStatus", "indexer not started");
|
||||
return {};
|
||||
}
|
||||
|
||||
// The FFI builds the status JSON itself (schema owned by indexer_core), so
|
||||
// there is nothing to marshal — copy the C string out and free it.
|
||||
char* json = ::query_status(handle(indexer_service_ffi));
|
||||
if (!json) {
|
||||
warn("getStatus", "indexer FFI error");
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::string out(json);
|
||||
::free_cstring(json);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string LezIndexerModuleImpl::getTransactionsByAccount(
|
||||
const std::string& account_id,
|
||||
const std::string& offset,
|
||||
const std::string& limit
|
||||
) {
|
||||
if (!indexer_service_ffi) {
|
||||
warn("getTransactionsByAccount", "indexer not started");
|
||||
return {};
|
||||
}
|
||||
|
||||
FfiAccountId id;
|
||||
if (!hexToBytes32(account_id, &id)) {
|
||||
warn("getTransactionsByAccount", "invalid account id (need 32-byte hex)");
|
||||
return {};
|
||||
}
|
||||
|
||||
char* offEnd = nullptr;
|
||||
char* limEnd = nullptr;
|
||||
const uint64_t offsetNum = std::strtoull(offset.c_str(), &offEnd, 10);
|
||||
const uint64_t limitNum = std::strtoull(limit.c_str(), &limEnd, 10);
|
||||
if (offEnd == offset.c_str() || *offEnd != '\0' || limEnd == limit.c_str() || *limEnd != '\0') {
|
||||
warn("getTransactionsByAccount", "invalid offset/limit");
|
||||
return {};
|
||||
}
|
||||
|
||||
PointerResult_FfiVec_FfiTransaction_____OperationStatus res =
|
||||
::query_transactions_by_account(handle(indexer_service_ffi), id, offsetNum, limitNum);
|
||||
if (is_error(&res.error) || !res.value) {
|
||||
warn("getTransactionsByAccount", "indexer FFI error");
|
||||
return {};
|
||||
}
|
||||
|
||||
nlohmann::json arr = nlohmann::json::array();
|
||||
for (uintptr_t i = 0; i < res.value->len; ++i) {
|
||||
arr.push_back(ffiTransactionToJson(res.value->entries[i]));
|
||||
}
|
||||
::free_ffi_transaction_vec(res.value);
|
||||
return jsonToCompactString(arr);
|
||||
}
|
||||
82
src/lez_indexer_module_impl.h
Normal file
82
src/lez_indexer_module_impl.h
Normal file
@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "logos_module_context.h"
|
||||
|
||||
/**
|
||||
* @brief Logos Execution Zone indexer module — wraps the LEZ indexer Rust FFI.
|
||||
*
|
||||
* Universal authoring model: this impl class is the whole module. Its public
|
||||
* methods ARE the API — callable by other modules (e.g. the LEZ explorer UI)
|
||||
* over the Logos protocol and from the CLI (`logoscore -c`). The Qt plugin glue
|
||||
* (the *Plugin / *Interface classes, Q_PLUGIN_METADATA, initLogos wiring) is
|
||||
* generated from this header by logos-module-builder.
|
||||
*
|
||||
* Module code is Qt-free (std::string, not QString). The FFI handle is held as
|
||||
* an opaque void* so this header stays free of the generated `indexer_ffi.h` —
|
||||
* the universal codegen parses this header, and we don't want it to depend on
|
||||
* the FFI types. The .cpp casts the handle back to `IndexerServiceFFI*`.
|
||||
*
|
||||
* Every query returns a compact JSON string; an EMPTY string means
|
||||
* not-found / failed query.
|
||||
*/
|
||||
class LezIndexerModuleImpl : public LogosModuleContext {
|
||||
public:
|
||||
~LezIndexerModuleImpl();
|
||||
|
||||
/// Boot ingestion against the indexer config at `config_path` (must be an
|
||||
/// ABSOLUTE path — the module runs in a logos_host subprocess). RocksDB state
|
||||
/// is stored under this module's instance persistence path (host-owned),
|
||||
/// independent of the config. Idempotent: a second call while already running
|
||||
/// is a no-op. Returns 0 on success, else the FFI OperationStatus code.
|
||||
/// int64_t (not int): the universal codegen marshals int64_t/std::string/bool
|
||||
/// as scalar wire types; a plain `int` return is treated as a JSON payload.
|
||||
int64_t start_indexer(const std::string& config_path);
|
||||
|
||||
/// Stop ingestion and release the FFI handle. No-op (returns 0) when the
|
||||
/// indexer isn't running. Pair with start_indexer to apply a new config:
|
||||
/// stop, then start (start_indexer stays idempotent and won't restart on its
|
||||
/// own). Returns 0 on success, else the FFI OperationStatus code.
|
||||
int64_t stop_indexer();
|
||||
|
||||
/// Account by 32-byte hex id. The returned JSON omits the id; callers inject
|
||||
/// the queried id themselves.
|
||||
std::string getAccount(const std::string& account_id);
|
||||
/// Block by decimal block id.
|
||||
std::string getBlockById(const std::string& block_id);
|
||||
/// Block by 32-byte hex hash.
|
||||
std::string getBlockByHash(const std::string& hash);
|
||||
/// Transaction by 32-byte hex hash.
|
||||
std::string getTransaction(const std::string& hash);
|
||||
/// Page of blocks: `before` = "" for the tip, else a block id to page back
|
||||
/// from; `limit` is the decimal max count.
|
||||
std::string getBlocks(const std::string& before, const std::string& limit);
|
||||
/// Tip block id as a bare decimal string.
|
||||
std::string getLastFinalizedBlockId();
|
||||
/// Current ingestion status as a compact JSON object so a UI can tell
|
||||
/// "catching up" from "failed": { state (starting/syncing/caught_up/error),
|
||||
/// indexedBlockId, lastError }. Empty string if the indexer isn't running.
|
||||
std::string getStatus();
|
||||
/// Transactions touching `account_id` (32-byte hex), paginated by decimal
|
||||
/// `offset`/`limit`.
|
||||
//
|
||||
// MUST stay on a single line: the universal `--header-to-lidl` parser drops
|
||||
// methods whose parameter list spans multiple lines, so a wrapped signature
|
||||
// silently vanishes from the published LIDL contract (and thus from typed
|
||||
// consumers like lez_explorer_ui). clang-format off keeps the linter from
|
||||
// re-wrapping this long line.
|
||||
// clang-format off
|
||||
std::string getTransactionsByAccount(const std::string& account_id, const std::string& offset, const std::string& limit);
|
||||
// clang-format on
|
||||
|
||||
/// Enable the indexer's logging at `level` (off/error/warn/info/debug/trace;
|
||||
/// null or unparseable falls back to info). Scoped to the indexer crates
|
||||
/// only; the first call wins (subsequent calls are no-ops).
|
||||
void init_logger(const std::string& level);
|
||||
|
||||
private:
|
||||
// IndexerServiceFFI* — opaque here (see class comment); cast in the .cpp.
|
||||
void* indexer_service_ffi = nullptr;
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user