Merge 7d2ae4a69d72998048a7a428735084104679d4ac into 08bd849d5a4d331d7c206cdd291105c95147dc12

This commit is contained in:
Iuri Matias 2026-05-06 13:49:21 -04:00 committed by GitHub
commit d053743700
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 2784 additions and 852 deletions

45
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,45 @@
name: CI
on:
push:
branches: [master]
pull_request:
branches: [master]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v27
with:
extra_nix_config: |
experimental-features = nix-command flakes
- uses: cachix/cachix-action@v15
with:
name: logos-co
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Build module
run: nix build
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v27
with:
extra_nix_config: |
experimental-features = nix-command flakes
- uses: cachix/cachix-action@v15
with:
name: logos-co
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Run unit tests
run: nix build '.#checks.x86_64-linux.unit-tests' -L

View File

@ -1,203 +1,23 @@
cmake_minimum_required(VERSION 3.20)
project(logos-blockchain-module LANGUAGES CXX)
cmake_minimum_required(VERSION 3.14)
project(LogosBlockchainModulePlugin LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# ---- Options ----
set(LOGOS_CORE_ROOT "" CACHE PATH "Path to logos-core root directory.")
set(LOGOS_BLOCKCHAIN_ROOT "" CACHE PATH "Path to logos-blockchain source root.")
set(LOGOS_BLOCKCHAIN_LIB "" CACHE PATH "Path to prebuilt logos-blockchain lib.")
set(LOGOS_BLOCKCHAIN_INCLUDE "" CACHE PATH "Path to prebuilt logos-blockchain include.")
set(HAS_LOGOS_CORE_ROOT FALSE)
set(HAS_LOGOS_BLOCKCHAIN_ROOT FALSE)
set(HAS_LOGOS_BLOCKCHAIN_LIB FALSE)
set(HAS_LOGOS_BLOCKCHAIN_INCLUDE FALSE)
if (DEFINED LOGOS_CORE_ROOT AND NOT "${LOGOS_CORE_ROOT}" STREQUAL "")
set(HAS_LOGOS_CORE_ROOT TRUE)
endif()
if (DEFINED LOGOS_BLOCKCHAIN_ROOT AND NOT "${LOGOS_BLOCKCHAIN_ROOT}" STREQUAL "")
set(HAS_LOGOS_BLOCKCHAIN_ROOT TRUE)
endif()
if(DEFINED LOGOS_BLOCKCHAIN_LIB AND NOT "${LOGOS_BLOCKCHAIN_LIB}" STREQUAL "")
set(HAS_LOGOS_BLOCKCHAIN_LIB TRUE)
endif()
if(DEFINED LOGOS_BLOCKCHAIN_INCLUDE AND NOT "${LOGOS_BLOCKCHAIN_INCLUDE}" STREQUAL "")
set(HAS_LOGOS_BLOCKCHAIN_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_BLOCKCHAIN_LIB AND HAS_LOGOS_BLOCKCHAIN_INCLUDE AND NOT HAS_LOGOS_BLOCKCHAIN_ROOT)
message(STATUS "Using prebuilt logos-blockchain.")
set(LOGOS_BLOCKCHAIN_PREBUILT TRUE)
elseif(NOT HAS_LOGOS_BLOCKCHAIN_LIB AND NOT HAS_LOGOS_BLOCKCHAIN_INCLUDE AND HAS_LOGOS_BLOCKCHAIN_ROOT)
message(STATUS "Building logos-blockchain from source.")
set(LOGOS_BLOCKCHAIN_PREBUILT FALSE)
if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT})
include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake)
else()
message(FATAL_ERROR "Either both LOGOS_BLOCKCHAIN_LIB and LOGOS_BLOCKCHAIN_INCLUDE must be set for prebuilt logos-blockchain, or only LOGOS_BLOCKCHAIN_ROOT must be set for building 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)
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_BLOCKCHAIN_PREBUILT)
set(LOGOS_BLOCKCHAIN_DYLIB "${LOGOS_BLOCKCHAIN_LIB}/liblogos_blockchain${DYLIB_EXT}")
if(WIN32)
set(LOGOS_BLOCKCHAIN_IMPLIB "${LOGOS_BLOCKCHAIN_LIB}/logos_blockchain${IMPLIB_EXT}")
endif()
add_custom_target(logos_blockchain_libs)
else()
find_program(CARGO_EXECUTABLE cargo REQUIRED)
set(CARGO_TARGET_DIR "${WORKSPACE_ROOT}/logos-blockchain/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_BLOCKCHAIN_LIB "${INTERNAL_STAGE_LIB}")
set(LOGOS_BLOCKCHAIN_INCLUDE "${INTERNAL_STAGE_INCLUDE}")
set(LOGOS_BLOCKCHAIN_DYLIB "${INTERNAL_STAGE_LIB}/liblogos_blockchain${DYLIB_EXT}")
set(LOGOS_BLOCKCHAIN_HEADER "${INTERNAL_STAGE_INCLUDE}/logos_blockchain.h")
add_custom_command(
OUTPUT "${LOGOS_BLOCKCHAIN_DYLIB}"
COMMAND ${CMAKE_COMMAND} -E env
CARGO_TARGET_DIR=${CARGO_TARGET_DIR}
${CARGO_EXECUTABLE} build --release
--package logos-blockchain-c
--manifest-path "${LOGOS_BLOCKCHAIN_ROOT}/Cargo.toml"
COMMAND ${CMAKE_COMMAND} -E copy
"${CARGO_TARGET_DIR}/release/liblogos_blockchain${DYLIB_EXT}"
"${LOGOS_BLOCKCHAIN_DYLIB}"
DEPENDS "${LOGOS_BLOCKCHAIN_ROOT}/Cargo.toml"
VERBATIM
)
add_custom_command(
OUTPUT "${LOGOS_BLOCKCHAIN_HEADER}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${LOGOS_BLOCKCHAIN_ROOT}/c-bindings/logos_blockchain.h"
"${LOGOS_BLOCKCHAIN_HEADER}"
DEPENDS "${LOGOS_BLOCKCHAIN_DYLIB}"
VERBATIM
)
add_custom_target(logos_blockchain_libs DEPENDS "${LOGOS_BLOCKCHAIN_DYLIB}" "${LOGOS_BLOCKCHAIN_HEADER}")
endif()
# ---- Imported targets ----
add_library(logos_blockchain_interface SHARED IMPORTED GLOBAL)
set_target_properties(logos_blockchain_interface PROPERTIES
IMPORTED_LOCATION "${LOGOS_BLOCKCHAIN_DYLIB}"
INTERFACE_INCLUDE_DIRECTORIES "${LOGOS_BLOCKCHAIN_INCLUDE}"
# logos_module() handles: Qt/AUTOMOC setup, SDK/module include paths, linking
# libs from EXTERNAL_LIBS into lib/, plugin output naming, RPATH, install rules.
logos_module(
NAME liblogos_blockchain_module
SOURCES
src/i_logos_blockchain_module.h
src/logos_blockchain_module.h
src/logos_blockchain_module.cpp
EXTERNAL_LIBS
logos_blockchain
)
if(NOT LOGOS_BLOCKCHAIN_PREBUILT)
add_dependencies(logos_blockchain_interface logos_blockchain_libs)
endif()
if(WIN32)
set_target_properties(logos_blockchain_interface PROPERTIES IMPORTED_IMPLIB "${LOGOS_BLOCKCHAIN_IMPLIB}")
endif()
add_library(logos_core STATIC IMPORTED)
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}" "${SDK_INC}/cpp")
# ---- Plugin ----
set(PLUGIN_TARGET logos_blockchain_module)
qt_add_plugin(${PLUGIN_TARGET} CLASS_NAME LogosBlockchainModule)
target_sources(${PLUGIN_TARGET} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/logos_blockchain_module.cpp
)
set_property(TARGET ${PLUGIN_TARGET} PROPERTY PUBLIC_HEADER
${CMAKE_CURRENT_SOURCE_DIR}/src/i_logos_blockchain_module.h
)
target_include_directories(${PLUGIN_TARGET} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
)
target_link_libraries(${PLUGIN_TARGET} PRIVATE
Qt6::Core
Qt6::RemoteObjects
logos_blockchain_interface
logos_cpp_sdk
logos_core
)
target_compile_definitions(${PLUGIN_TARGET} PRIVATE
LOGOS_BLOCKCHAIN_MODULE_METADATA_FILE="${CMAKE_CURRENT_SOURCE_DIR}/metadata.json"
)
add_dependencies(${PLUGIN_TARGET} logos_blockchain_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_BLOCKCHAIN_INCLUDE}/" DESTINATION include)
install(FILES "${LOGOS_BLOCKCHAIN_DYLIB}" DESTINATION lib)

View File

@ -1,38 +1,10 @@
# Logos Blockchain Module
### Setup
A Logos core module that wraps the [logos-blockchain](https://github.com/logos-blockchain/logos-blockchain) C bindings and ships the zk circuit binaries needed at runtime.
#### IDE
### Build and inspect
If you're using an IDE with CMake integration make sure it points to the same cmake directory as the `justfile`, which defaults to `build`.
```bash
nix build '.#lgx'
```
This will reduce friction when working on the project.
#### Nix
* Use `nix flake update` to bring all nix context and packages
* Use `nix build` to build the package
* Use `nix run` to launch the module-viewer and check your module loads properly
* Use `nix develop` to setup your IDE
### Troubleshooting
#### Nix + IDE Integration
If your IDE reports that a file doesn't belong to the project or that files cannot be found, the CMake cache
is likely missing the Nix-provided paths. This happens when the IDE runs CMake on its own, outside the Nix
environment, leaving the required paths empty.
To fix it:
1. **Regenerate the cache from within the Nix shell**
This provides the required Nix paths and writes them into `build/CMakeCache.txt`:
```bash
nix develop -c just configure
```
2. **Reload the CMake project without resetting the cache**
If on RustRover: Open the CMake tool window (**View → Tool Windows → CMake**) and click the **Reload** button (↺) in the toolbar.
> Resetting the cache would wipe the paths you just wrote, so make sure to reload only.

2018
flake.lock generated

File diff suppressed because it is too large Load Diff

183
flake.nix
View File

@ -2,162 +2,45 @@
description = "Logos Blockchain Module - Qt6 Plugin";
inputs = {
nixpkgs.follows = "logos-liblogos/nixpkgs";
logos-liblogos.url = "github:logos-co/logos-liblogos";
logos-core.url = "github:logos-co/logos-cpp-sdk";
logos-module-builder.url = "github:logos-co/logos-module-builder";
logos-blockchain.url = "github:logos-blockchain/logos-blockchain?rev=88941ff33f2e028591b9d0ed2549a328d54f0cfa"; # pre-0.1.3 + potential note fixes
logos-module-viewer.url = "github:logos-co/logos-module-viewer";
};
outputs =
{
self,
nixpkgs,
logos-core,
logos-blockchain,
logos-module-viewer,
...
}:
let
lib = nixpkgs.lib;
outputs = inputs@{ logos-module-builder, ... }:
logos-module-builder.lib.mkLogosModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
systems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
externalLibInputs = {
logos_blockchain = inputs.logos-blockchain;
};
forAll = lib.genAttrs systems;
tests = {
dir = ./tests;
mockCLibs = [ "logos_blockchain" ];
};
mkPkgs = system: import nixpkgs { inherit system; };
in
{
packages = forAll (
system:
let
pkgs = mkPkgs system;
llvmPkgs = pkgs.llvmPackages;
preConfigure = { externalLibs }: ''
if [ -d "${externalLibs.logos_blockchain}/circuits" ]; then
echo "Staging zk circuits from logos-blockchain..."
cp -r "${externalLibs.logos_blockchain}/circuits" ./circuits
chmod -R u+w ./circuits
else
echo "WARNING: no circuits/ found in logos-blockchain derivation"
fi
'';
logosCore = logos-core.packages.${system}.default;
logosBlockchainC = logos-blockchain.packages.${system}.logos-blockchain-c;
logosBlockchainModule = pkgs.stdenv.mkDerivation {
pname = "logos-blockchain-module";
version = "dev";
src = ./.;
nativeBuildInputs = [
pkgs.cmake
pkgs.ninja
pkgs.pkg-config
pkgs.qt6.wrapQtAppsHook
];
buildInputs = [
pkgs.qt6.qtbase
pkgs.qt6.qtremoteobjects
pkgs.qt6.qttools
llvmPkgs.clang
llvmPkgs.libclang
logosBlockchainC
]
++ 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_BLOCKCHAIN_LIB=${logosBlockchainC}/lib"
"-DLOGOS_BLOCKCHAIN_INCLUDE=${logosBlockchainC}/include"
];
postInstall = ''
mkdir $out/share
cp -r ${logosBlockchainC}/circuits $out/share
'';
# Logos Core Edge-case
# The current version of Logos Core expects circuits' binaries under `lib/circuits/`.
# Until we address this in Logos Core, we use this hook to include to ensure the circuits' binaries
# are included in the binary bundle and avoid the circuits being mangled by Nix (which did that when
# copying them in a previous phase).
postFixup = ''
cp -r ${logosBlockchainC}/circuits $out/lib/circuits
'';
};
in
{
lib = logosBlockchainModule;
default = logosBlockchainModule;
}
);
apps = forAll (
system:
let
pkgs = mkPkgs system;
logosBlockchainModuleLib = self.packages.${system}.lib;
logosModuleViewer = 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 ${logosModuleViewer}/bin/logos-module-viewer \
--module ${logosBlockchainModuleLib}/lib/liblogos_blockchain_module.${extension}
''}/bin/inspect-module";
};
in
{
inspect-module = inspectModule;
default = inspectModule;
}
);
devShells = forAll (
system:
let
pkgs = mkPkgs system;
pkg = self.packages.${system}.default;
logosCore = logos-core.packages.${system}.default;
logosBlockchainC = logos-blockchain.packages.${system}.logos-blockchain-c;
in
{
default = pkgs.mkShell {
inputsFrom = [ pkg ];
inherit (pkg)
LIBCLANG_PATH
CLANG_PATH;
LOGOS_CORE_ROOT = "${logosCore}";
LOGOS_BLOCKCHAIN_LIB = "${logosBlockchainC}/lib";
LOGOS_BLOCKCHAIN_INCLUDE = "${logosBlockchainC}/include";
shellHook = ''
BLUE='\e[1;34m'
GREEN='\e[1;32m'
RESET='\e[0m'
echo -e "\n''${BLUE}=== Logos Blockchain Module Development Environment ===''${RESET}"
echo -e "''${GREEN}LOGOS_CORE_ROOT:''${RESET} $LOGOS_CORE_ROOT"
echo -e "''${GREEN}LOGOS_BLOCKCHAIN_LIB:''${RESET} $LOGOS_BLOCKCHAIN_LIB"
echo -e "''${GREEN}LOGOS_BLOCKCHAIN_INCLUDE:''${RESET} $LOGOS_BLOCKCHAIN_INCLUDE"
echo -e "''${BLUE}---------------------------------------------------------''${RESET}"
'';
};
}
);
# Logos Core Edge-case
# The current version of Logos Core expects circuits' binaries under `lib/circuits/`.
# Until we address this in Logos Core, we use this hook to include to ensure the circuits' binaries
# are included in the binary bundle and avoid the circuits being mangled by Nix (which did that when
# copying them in a previous phase).
postInstall = ''
if [ -d "$LOGOS_MODULE_SOURCE_DIR/circuits" ]; then
cp -r "$LOGOS_MODULE_SOURCE_DIR/circuits" "$out/lib/circuits"
chmod -R u+w "$out/lib/circuits"
fi
'';
};
}

View File

@ -1,13 +1,13 @@
default: build
# Inside `nix develop` / `ws develop logos-blockchain-module` the module-builder
# provides LOGOS_CPP_SDK_ROOT and LOGOS_MODULE_BUILDER_ROOT — CMake picks them
# up automatically via the logos_module() macro, no explicit -D flags needed.
configure:
cmake -S . -B build -G Ninja \
${LOGOS_CORE_ROOT:+-DLOGOS_CORE_ROOT="$LOGOS_CORE_ROOT"} \
${LOGOS_BLOCKCHAIN_LIB:+-DLOGOS_BLOCKCHAIN_LIB="$LOGOS_BLOCKCHAIN_LIB"} \
${LOGOS_BLOCKCHAIN_INCLUDE:+-DLOGOS_BLOCKCHAIN_INCLUDE="$LOGOS_BLOCKCHAIN_INCLUDE"}
cmake -S . -B build -G Ninja
build: configure
cmake --build build --parallel --target logos_blockchain_module
cmake --build build --parallel --target liblogos_blockchain_module_module_plugin
clean:
rm -rf build result

View File

@ -5,16 +5,29 @@
"author": "Logos Blockchain Team",
"type": "core",
"category": "blockchain",
"main": "liblogos_blockchain_module",
"main": "liblogos_blockchain_module_plugin",
"dependencies": [],
"capabilities": [],
"include": [
"liblogos_blockchain.dylib",
"liblogos_blockchain.so",
"liblogos_blockchain.dll",
"liblogos_blockchain_module.dylib",
"liblogos_blockchain_module.so",
"liblogos_blockchain_module.dll",
"liblogos_blockchain_module_plugin.dylib",
"liblogos_blockchain_module_plugin.so",
"liblogos_blockchain_module_plugin.dll",
"circuits"
]
}
],
"nix": {
"packages": {
"build": [],
"runtime": []
},
"external_libraries": [
{ "name": "logos_blockchain" }
],
"cmake": {
"extra_include_dirs": ["lib"]
}
}
}

View File

@ -2,7 +2,7 @@
#define I_LOGOS_BLOCKCHAIN_MODULE_API_H
#include <QString>
#include <core/interface.h>
#include "interface.h"
class ILogosBlockchainModule {
public:

View File

@ -4,12 +4,31 @@
#include <QDir>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QMetaType>
#include <QUrl>
#include <QVariant>
#include <QVariantList>
// Define static member
LogosBlockchainModule* LogosBlockchainModule::s_instance = nullptr;
namespace {
// Rust `File::open` / `deserialize_config_at_path` only accept real filesystem paths. QML often
// passes `file:///...` URLs; strip to a local path when applicable.
QString localPathFromFileUrl(const QString& s) {
if (s.isEmpty()) {
return s;
}
if (s.startsWith(QStringLiteral("file:"), Qt::CaseInsensitive)) {
const QUrl u(s);
if (u.isLocalFile()) {
return QDir::toNativeSeparators(u.toLocalFile());
}
}
return s;
}
// Use the C API type Hash (from logos_blockchain.h) to define address/hash byte size.
constexpr int ADDRESS_BYTES = sizeof(Hash);
constexpr int ADDRESS_HEX_LEN = ADDRESS_BYTES * 2;
@ -263,14 +282,14 @@ int LogosBlockchainModule::start(const QString& config_path, const QString& depl
}
}
qInfo() << "Starting the node with the configuration file:" << effective_config_path;
qInfo() << "Using deployment:" << (deployment.isEmpty() ? "<default>" : deployment);
effective_config_path = localPathFromFileUrl(effective_config_path);
const QString deployment_path = localPathFromFileUrl(deployment);
const QByteArray config_path_buffer = effective_config_path.toUtf8();
const char* config_path_ptr = effective_config_path.isEmpty() ? nullptr : config_path_buffer.constData();
const QByteArray deployment_buffer = deployment.toUtf8();
const char* deployment_ptr = deployment.isEmpty() ? nullptr : deployment_buffer.constData();
const QByteArray deployment_buffer = deployment_path.toUtf8();
const char* deployment_ptr = deployment_path.isEmpty() ? nullptr : deployment_buffer.constData();
auto [value, error] = start_lb_node(config_path_ptr, deployment_ptr);
qInfo() << "Start node returned with value and error.";

View File

@ -13,7 +13,7 @@ extern "C" {
class LogosBlockchainModule final : public QObject, public PluginInterface, public ILogosBlockchainModule {
Q_OBJECT
Q_PLUGIN_METADATA(IID ILogosBlockchainModule_iid FILE LOGOS_BLOCKCHAIN_MODULE_METADATA_FILE)
Q_PLUGIN_METADATA(IID ILogosBlockchainModule_iid FILE "metadata.json")
Q_INTERFACES(PluginInterface)
public:

50
tests/CMakeLists.txt Normal file
View File

@ -0,0 +1,50 @@
cmake_minimum_required(VERSION 3.14)
project(BlockchainModuleTests LANGUAGES CXX)
include(LogosTest)
# Unit tests (mocked logos_blockchain)
logos_test(
NAME blockchain_module_tests
MODULE_SOURCES
../src/logos_blockchain_module.cpp
TEST_SOURCES
main.cpp
test_blockchain.cpp
MOCK_C_SOURCES
mocks/mock_logos_blockchain.cpp
EXTRA_INCLUDES
stubs
)
# Integration tests (real logos_blockchain library)
find_library(LIBLOGOS_BLOCKCHAIN_PATH
NAMES liblogos_blockchain.so liblogos_blockchain.dylib
PATHS ${CMAKE_CURRENT_SOURCE_DIR}/../lib
NO_DEFAULT_PATH)
if(LIBLOGOS_BLOCKCHAIN_PATH)
message(STATUS "[BlockchainTests] logos_blockchain found: ${LIBLOGOS_BLOCKCHAIN_PATH} - building integration tests")
logos_test(
NAME blockchain_module_integration_tests
MODULE_SOURCES
../src/logos_blockchain_module.cpp
TEST_SOURCES
main.cpp
test_blockchain.cpp
EXTRA_INCLUDES
../lib
EXTRA_LINK_LIBS
${LIBLOGOS_BLOCKCHAIN_PATH}
)
get_filename_component(LIBLOGOS_BLOCKCHAIN_DIR "${LIBLOGOS_BLOCKCHAIN_PATH}" DIRECTORY)
set_target_properties(blockchain_module_integration_tests PROPERTIES
BUILD_RPATH "${LIBLOGOS_BLOCKCHAIN_DIR}"
)
else()
message(STATUS "[BlockchainTests] logos_blockchain not found in ../lib - skipping integration tests")
endif()

3
tests/main.cpp Normal file
View File

@ -0,0 +1,3 @@
#include <logos_test.h>
LOGOS_TEST_MAIN()

View File

@ -0,0 +1,159 @@
// Mock implementation of logos_blockchain C functions.
// Replaces the real Rust library at link time during unit tests.
// Return values are controlled via LogosCMockStore.
#include <logos_clib_mock.h>
#include <logos_blockchain.h>
#include <cstring>
#include <cstdlib>
static char s_fakeNode = 0;
static CryptarchiaInfo s_fakeCryptarchiaInfo = {};
// Known-address mock storage (up to 4 addresses)
static uint8_t s_mockAddr0[32];
static uint8_t s_mockAddr1[32];
static uint8_t s_mockAddr2[32];
static uint8_t s_mockAddr3[32];
static uint8_t* s_mockAddrs[] = { s_mockAddr0, s_mockAddr1, s_mockAddr2, s_mockAddr3 };
extern "C" {
bool is_ok(const OperationStatus* status) {
return status && *status == 0;
}
OperationStatus generate_user_config(GenerateConfigArgs args) {
LOGOS_CMOCK_RECORD("generate_user_config");
return LOGOS_CMOCK_RETURN(int, "generate_user_config");
}
NodeResult start_lb_node(const char* config_path, const char* deployment) {
LOGOS_CMOCK_RECORD("start_lb_node");
int ok = LOGOS_CMOCK_RETURN(int, "start_lb_node");
NodeResult result;
result.value = ok ? reinterpret_cast<LogosBlockchainNode*>(&s_fakeNode) : nullptr;
result.error = ok ? 0 : 1;
return result;
}
OperationStatus stop_node(LogosBlockchainNode* node) {
LOGOS_CMOCK_RECORD("stop_node");
return 0;
}
OperationStatus subscribe_to_new_blocks(LogosBlockchainNode* node, BlockCallback callback) {
LOGOS_CMOCK_RECORD("subscribe_to_new_blocks");
return LOGOS_CMOCK_RETURN(int, "subscribe_to_new_blocks");
}
BalanceResult get_balance(LogosBlockchainNode* node, const uint8_t* address, const void* reserved) {
LOGOS_CMOCK_RECORD("get_balance");
BalanceResult result;
result.value = static_cast<uint64_t>(LOGOS_CMOCK_RETURN(int, "get_balance_value"));
result.error = LOGOS_CMOCK_RETURN(int, "get_balance_error");
return result;
}
TransferHashResult transfer_funds(LogosBlockchainNode* node, const TransferFundsArguments* args) {
LOGOS_CMOCK_RECORD("transfer_funds");
TransferHashResult result;
memset(result.value, 0xAB, sizeof(Hash));
result.error = LOGOS_CMOCK_RETURN(int, "transfer_funds_error");
return result;
}
KnownAddressesResult get_known_addresses(LogosBlockchainNode* node) {
LOGOS_CMOCK_RECORD("get_known_addresses");
KnownAddressesResult result;
int err = LOGOS_CMOCK_RETURN(int, "get_known_addresses_error");
result.error = err;
if (err == 0) {
int count = LOGOS_CMOCK_RETURN(int, "get_known_addresses_count");
if (count > 4) count = 4;
memset(s_mockAddr0, 0x11, 32);
memset(s_mockAddr1, 0x22, 32);
memset(s_mockAddr2, 0x33, 32);
memset(s_mockAddr3, 0x44, 32);
result.value.addresses = s_mockAddrs;
result.value.len = static_cast<size_t>(count);
} else {
result.value.addresses = nullptr;
result.value.len = 0;
}
return result;
}
OperationStatus free_known_addresses(KnownAddresses addrs) {
LOGOS_CMOCK_RECORD("free_known_addresses");
return 0;
}
BlendHashResult blend_join_as_core_node(
LogosBlockchainNode* node,
const uint8_t* provider_id,
const uint8_t* zk_id,
const uint8_t* locked_note_id,
const char** locators,
size_t locators_count)
{
LOGOS_CMOCK_RECORD("blend_join_as_core_node");
BlendHashResult result;
memset(result.value, 0xCD, sizeof(Hash));
result.error = LOGOS_CMOCK_RETURN(int, "blend_join_as_core_node_error");
return result;
}
StringResult get_block(LogosBlockchainNode* node, const HeaderId* header_id) {
LOGOS_CMOCK_RECORD("get_block");
StringResult result;
const char* json = LOGOS_CMOCK_RETURN_STRING("get_block");
result.value = json ? strdup(json) : nullptr;
result.error = LOGOS_CMOCK_RETURN(int, "get_block_error");
return result;
}
StringResult get_blocks(LogosBlockchainNode* node, uint64_t from_slot, uint64_t to_slot) {
LOGOS_CMOCK_RECORD("get_blocks");
StringResult result;
const char* json = LOGOS_CMOCK_RETURN_STRING("get_blocks");
result.value = json ? strdup(json) : nullptr;
result.error = LOGOS_CMOCK_RETURN(int, "get_blocks_error");
return result;
}
StringResult get_transaction(LogosBlockchainNode* node, const TxHash* tx_hash) {
LOGOS_CMOCK_RECORD("get_transaction");
StringResult result;
const char* json = LOGOS_CMOCK_RETURN_STRING("get_transaction");
result.value = json ? strdup(json) : nullptr;
result.error = LOGOS_CMOCK_RETURN(int, "get_transaction_error");
return result;
}
CryptarchiaInfoResult get_cryptarchia_info(LogosBlockchainNode* node) {
LOGOS_CMOCK_RECORD("get_cryptarchia_info");
CryptarchiaInfoResult result;
memset(&s_fakeCryptarchiaInfo, 0, sizeof(s_fakeCryptarchiaInfo));
s_fakeCryptarchiaInfo.slot = static_cast<uint64_t>(LOGOS_CMOCK_RETURN(int, "cryptarchia_slot"));
s_fakeCryptarchiaInfo.height = static_cast<uint64_t>(LOGOS_CMOCK_RETURN(int, "cryptarchia_height"));
s_fakeCryptarchiaInfo.mode = static_cast<State>(LOGOS_CMOCK_RETURN(int, "cryptarchia_mode"));
memset(s_fakeCryptarchiaInfo.lib, 0xEE, 32);
memset(s_fakeCryptarchiaInfo.tip, 0xFF, 32);
result.value = &s_fakeCryptarchiaInfo;
result.error = LOGOS_CMOCK_RETURN(int, "get_cryptarchia_info_error");
return result;
}
OperationStatus free_cryptarchia_info(CryptarchiaInfo* info) {
LOGOS_CMOCK_RECORD("free_cryptarchia_info");
return 0;
}
OperationStatus free_cstring(char* s) {
LOGOS_CMOCK_RECORD("free_cstring");
free(s);
return 0;
}
} // extern "C"

View File

@ -0,0 +1,131 @@
// Stub header for logos_blockchain — provides the same declarations as the real
// Rust-generated header so that logos_blockchain_module sources compile in tests.
#ifndef LOGOS_BLOCKCHAIN_H
#define LOGOS_BLOCKCHAIN_H
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
// 32-byte hash/address types
typedef uint8_t Hash[32];
typedef uint8_t HeaderId[32];
typedef uint8_t TxHash[32];
// Opaque node handle
typedef struct LogosBlockchainNode LogosBlockchainNode;
// Operation status (0 = OK)
typedef int OperationStatus;
// Deployment enums
typedef enum { WellKnown, Custom } DeploymentType;
typedef enum { Devnet } WellKnownDeployment;
// Consensus state enum
typedef enum { Bootstrapping, Online } State;
// Deployment configuration
typedef struct {
DeploymentType deployment_type;
WellKnownDeployment well_known_deployment;
const char* custom_deployment_config_path;
} Deployment;
// Arguments for generate_user_config
typedef struct {
const char** initial_peers;
const uint32_t* initial_peers_count;
const char* output;
const uint16_t* net_port;
const uint16_t* blend_port;
const char* http_addr;
const char* external_address;
const bool* no_public_ip_check;
const Deployment* deployment;
const char* state_path;
} GenerateConfigArgs;
// Arguments for transfer_funds
typedef struct {
const HeaderId* optional_tip;
const uint8_t* change_public_key;
const uint8_t* const* funding_public_keys;
size_t funding_public_keys_len;
const uint8_t* recipient_public_key;
uint64_t amount;
} TransferFundsArguments;
// Known addresses result container
typedef struct {
uint8_t** addresses;
size_t len;
} KnownAddresses;
// Cryptarchia consensus info
typedef struct {
uint8_t lib[32];
uint8_t tip[32];
uint64_t slot;
uint64_t height;
State mode;
} CryptarchiaInfo;
// Result types (C++ structured bindings decompose these)
typedef struct { LogosBlockchainNode* value; OperationStatus error; } NodeResult;
typedef struct { uint64_t value; OperationStatus error; } BalanceResult;
typedef struct { Hash value; OperationStatus error; } TransferHashResult;
typedef struct { KnownAddresses value; OperationStatus error; } KnownAddressesResult;
typedef struct { Hash value; OperationStatus error; } BlendHashResult;
typedef struct { char* value; OperationStatus error; } StringResult;
typedef struct { CryptarchiaInfo* value; OperationStatus error; } CryptarchiaInfoResult;
// Block event callback
typedef void (*BlockCallback)(const char* block_json);
// Status check
bool is_ok(const OperationStatus* status);
// Lifecycle
OperationStatus generate_user_config(GenerateConfigArgs args);
NodeResult start_lb_node(const char* config_path, const char* deployment);
OperationStatus stop_node(LogosBlockchainNode* node);
OperationStatus subscribe_to_new_blocks(LogosBlockchainNode* node, BlockCallback callback);
// Wallet
BalanceResult get_balance(LogosBlockchainNode* node, const uint8_t* address, const void* reserved);
TransferHashResult transfer_funds(LogosBlockchainNode* node, const TransferFundsArguments* args);
KnownAddressesResult get_known_addresses(LogosBlockchainNode* node);
OperationStatus free_known_addresses(KnownAddresses addrs);
// Blend
BlendHashResult blend_join_as_core_node(
LogosBlockchainNode* node,
const uint8_t* provider_id,
const uint8_t* zk_id,
const uint8_t* locked_note_id,
const char** locators,
size_t locators_count);
// Explorer
StringResult get_block(LogosBlockchainNode* node, const HeaderId* header_id);
StringResult get_blocks(LogosBlockchainNode* node, uint64_t from_slot, uint64_t to_slot);
StringResult get_transaction(LogosBlockchainNode* node, const TxHash* tx_hash);
// Cryptarchia
CryptarchiaInfoResult get_cryptarchia_info(LogosBlockchainNode* node);
OperationStatus free_cryptarchia_info(CryptarchiaInfo* info);
// Memory management
OperationStatus free_cstring(char* s);
#ifdef __cplusplus
}
#endif
#endif // LOGOS_BLOCKCHAIN_H

733
tests/test_blockchain.cpp Normal file
View File

@ -0,0 +1,733 @@
// Unit tests for LogosBlockchainModule.
// All logos_blockchain C functions are mocked at link time via mock_logos_blockchain.cpp.
#include <logos_test.h>
#include "logos_blockchain_module.h"
#include <QDir>
#include <QFile>
#include <QTemporaryDir>
// 64-char hex string = 32 bytes (valid address/hash)
static const QString VALID_HEX = QString(64, 'a');
static const QString VALID_HEX_WITH_PREFIX = "0x" + QString(64, 'b');
// Helper: create a module with a running (mocked) node.
// Sets up circuits directory, mock LogosAPI, and calls start().
static LogosBlockchainModule* createStartedModule(LogosTestContext& t, QTemporaryDir& tmpDir) {
QDir dir(tmpDir.path());
dir.mkpath("circuits");
QFile f(dir.filePath("circuits/dummy.bin"));
f.open(QIODevice::WriteOnly);
f.write("x");
f.close();
t.api()->setProperty("modulePath", tmpDir.path());
auto* module = new LogosBlockchainModule();
t.initLegacy(module);
t.mockCFunction("start_lb_node").returns(1);
t.mockCFunction("subscribe_to_new_blocks").returns(0);
int rc = module->start(tmpDir.filePath("config.json"), "");
if (rc != 0) {
delete module;
return nullptr;
}
return module;
}
// ============================================================================
// Core metadata
// ============================================================================
LOGOS_TEST(name_returns_module_name) {
LogosBlockchainModule module;
LOGOS_ASSERT_EQ(module.name(), QString("liblogos_blockchain_module"));
}
LOGOS_TEST(version_returns_module_version) {
LogosBlockchainModule module;
LOGOS_ASSERT_EQ(module.version(), QString("1.0.0"));
}
// ============================================================================
// generate_user_config
// ============================================================================
LOGOS_TEST(generate_user_config_returns_0_on_success) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
t.mockCFunction("generate_user_config").returns(0);
QVariantMap args;
args["output"] = "/tmp/test-config.json";
LOGOS_ASSERT_EQ(module.generate_user_config(args), 0);
LOGOS_ASSERT(t.cFunctionCalled("generate_user_config"));
}
LOGOS_TEST(generate_user_config_returns_1_on_failure) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
t.mockCFunction("generate_user_config").returns(1);
QVariantMap args;
LOGOS_ASSERT_EQ(module.generate_user_config(args), 1);
}
LOGOS_TEST(generate_user_config_from_str_delegates_to_generate_user_config) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
t.mockCFunction("generate_user_config").returns(0);
LOGOS_ASSERT_EQ(module.generate_user_config_from_str(R"({"output":"/tmp/out.json"})"), 0);
LOGOS_ASSERT(t.cFunctionCalled("generate_user_config"));
}
LOGOS_TEST(generate_user_config_with_all_fields) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
t.mockCFunction("generate_user_config").returns(0);
QVariantMap deployment;
deployment["well_known_deployment"] = "devnet";
QVariantMap args;
args["initial_peers"] = QStringList{"peer1", "peer2"};
args["output"] = "/tmp/out.json";
args["net_port"] = 9000;
args["blend_port"] = 9001;
args["http_addr"] = "0.0.0.0:8080";
args["external_address"] = "1.2.3.4";
args["no_public_ip_check"] = true;
args["deployment"] = deployment;
args["state_path"] = "/tmp/state";
LOGOS_ASSERT_EQ(module.generate_user_config(args), 0);
}
// ============================================================================
// No-node error paths — all methods should fail gracefully
// ============================================================================
LOGOS_TEST(stop_without_node_returns_1) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
LOGOS_ASSERT_EQ(module.stop(), 1);
}
LOGOS_TEST(wallet_get_balance_without_node_returns_error) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
QString result = module.wallet_get_balance(VALID_HEX);
LOGOS_ASSERT_TRUE(result.startsWith("Error:"));
LOGOS_ASSERT_TRUE(result.contains("not running"));
}
LOGOS_TEST(wallet_transfer_funds_without_node_returns_error) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
QString result = module.wallet_transfer_funds(VALID_HEX, QStringList{VALID_HEX}, VALID_HEX, "100", "");
LOGOS_ASSERT_TRUE(result.startsWith("Error:"));
LOGOS_ASSERT_TRUE(result.contains("not running"));
}
LOGOS_TEST(wallet_transfer_funds_single_sender_without_node_returns_error) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
QString result = module.wallet_transfer_funds(VALID_HEX, VALID_HEX, VALID_HEX, "100", "");
LOGOS_ASSERT_TRUE(result.startsWith("Error:"));
}
LOGOS_TEST(wallet_get_known_addresses_without_node_returns_empty) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
LOGOS_ASSERT_TRUE(module.wallet_get_known_addresses().isEmpty());
}
LOGOS_TEST(blend_join_as_core_node_without_node_returns_error) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
QString result = module.blend_join_as_core_node(VALID_HEX, VALID_HEX, VALID_HEX, {"locator1"});
LOGOS_ASSERT_TRUE(result.startsWith("Error:"));
LOGOS_ASSERT_TRUE(result.contains("not running"));
}
LOGOS_TEST(get_block_without_node_returns_error) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
LOGOS_ASSERT_TRUE(module.get_block(VALID_HEX).startsWith("Error:"));
}
LOGOS_TEST(get_blocks_without_node_returns_error) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
LOGOS_ASSERT_TRUE(module.get_blocks(0, 10).startsWith("Error:"));
}
LOGOS_TEST(get_transaction_without_node_returns_error) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
LOGOS_ASSERT_TRUE(module.get_transaction(VALID_HEX).startsWith("Error:"));
}
LOGOS_TEST(get_cryptarchia_info_without_node_returns_error) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
LOGOS_ASSERT_TRUE(module.get_cryptarchia_info().startsWith("Error:"));
}
// ============================================================================
// Node lifecycle (start / stop)
// ============================================================================
LOGOS_TEST(start_succeeds_with_mocked_dependencies) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
LOGOS_ASSERT_TRUE(tmpDir.isValid());
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
LOGOS_ASSERT(t.cFunctionCalled("start_lb_node"));
LOGOS_ASSERT(t.cFunctionCalled("subscribe_to_new_blocks"));
delete module;
}
LOGOS_TEST(start_returns_1_when_already_running) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
LOGOS_ASSERT_EQ(module->start("/tmp/config.json", ""), 1);
delete module;
}
LOGOS_TEST(start_returns_2_without_logos_api) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
LOGOS_ASSERT_EQ(module.start("/tmp/config.json", ""), 2);
}
LOGOS_TEST(stop_succeeds_with_running_node) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
LOGOS_ASSERT_EQ(module->stop(), 0);
LOGOS_ASSERT(t.cFunctionCalled("stop_node"));
delete module;
}
// ============================================================================
// Input validation (requires running node)
// ============================================================================
// wallet_get_balance validation
LOGOS_TEST(wallet_get_balance_rejects_short_hex) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
QString result = module->wallet_get_balance("abcd");
LOGOS_ASSERT_TRUE(result.startsWith("Error:"));
LOGOS_ASSERT_TRUE(result.contains("64 hex"));
delete module;
}
LOGOS_TEST(wallet_get_balance_rejects_long_hex) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
QString result = module->wallet_get_balance(QString(66, 'a'));
LOGOS_ASSERT_TRUE(result.startsWith("Error:"));
delete module;
}
LOGOS_TEST(wallet_get_balance_rejects_invalid_chars) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
QString hex = QString(62, 'a') + "zz";
QString result = module->wallet_get_balance(hex);
LOGOS_ASSERT_TRUE(result.startsWith("Error:"));
delete module;
}
// wallet_transfer_funds validation
LOGOS_TEST(wallet_transfer_funds_rejects_invalid_amount) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
QString result = module->wallet_transfer_funds(VALID_HEX, QStringList{VALID_HEX}, VALID_HEX, "not_a_number", "");
LOGOS_ASSERT_TRUE(result.startsWith("Error:"));
LOGOS_ASSERT_TRUE(result.contains("Invalid amount"));
delete module;
}
LOGOS_TEST(wallet_transfer_funds_rejects_invalid_change_key) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
QString result = module->wallet_transfer_funds("bad", QStringList{VALID_HEX}, VALID_HEX, "100", "");
LOGOS_ASSERT_TRUE(result.startsWith("Error:"));
LOGOS_ASSERT_TRUE(result.contains("change_public_key"));
delete module;
}
LOGOS_TEST(wallet_transfer_funds_rejects_invalid_recipient) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
QString result = module->wallet_transfer_funds(VALID_HEX, QStringList{VALID_HEX}, "short", "100", "");
LOGOS_ASSERT_TRUE(result.startsWith("Error:"));
LOGOS_ASSERT_TRUE(result.contains("recipient_address"));
delete module;
}
LOGOS_TEST(wallet_transfer_funds_rejects_empty_senders) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
QString result = module->wallet_transfer_funds(VALID_HEX, QStringList{}, VALID_HEX, "100", "");
LOGOS_ASSERT_TRUE(result.startsWith("Error:"));
LOGOS_ASSERT_TRUE(result.contains("sender"));
delete module;
}
LOGOS_TEST(wallet_transfer_funds_rejects_invalid_sender_address) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
QString result = module->wallet_transfer_funds(VALID_HEX, QStringList{"bad_addr"}, VALID_HEX, "100", "");
LOGOS_ASSERT_TRUE(result.startsWith("Error:"));
LOGOS_ASSERT_TRUE(result.contains("sender"));
delete module;
}
LOGOS_TEST(wallet_transfer_funds_rejects_invalid_optional_tip) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
QString result = module->wallet_transfer_funds(VALID_HEX, QStringList{VALID_HEX}, VALID_HEX, "100", "bad_tip");
LOGOS_ASSERT_TRUE(result.startsWith("Error:"));
LOGOS_ASSERT_TRUE(result.contains("tip"));
delete module;
}
// blend_join_as_core_node validation
LOGOS_TEST(blend_join_rejects_invalid_provider_id) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
QString result = module->blend_join_as_core_node("short", VALID_HEX, VALID_HEX, {});
LOGOS_ASSERT_TRUE(result.startsWith("Error:"));
LOGOS_ASSERT_TRUE(result.contains("provider_id"));
delete module;
}
LOGOS_TEST(blend_join_rejects_invalid_zk_id) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
QString result = module->blend_join_as_core_node(VALID_HEX, "short", VALID_HEX, {});
LOGOS_ASSERT_TRUE(result.startsWith("Error:"));
LOGOS_ASSERT_TRUE(result.contains("zk_id"));
delete module;
}
LOGOS_TEST(blend_join_rejects_invalid_locked_note_id) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
QString result = module->blend_join_as_core_node(VALID_HEX, VALID_HEX, "short", {});
LOGOS_ASSERT_TRUE(result.startsWith("Error:"));
LOGOS_ASSERT_TRUE(result.contains("locked_note_id"));
delete module;
}
// get_block / get_transaction validation
LOGOS_TEST(get_block_rejects_invalid_hex) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
QString result = module->get_block("tooshort");
LOGOS_ASSERT_TRUE(result.startsWith("Error:"));
LOGOS_ASSERT_TRUE(result.contains("64 hex"));
delete module;
}
LOGOS_TEST(get_transaction_rejects_invalid_hex) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
QString result = module->get_transaction("bad");
LOGOS_ASSERT_TRUE(result.startsWith("Error:"));
LOGOS_ASSERT_TRUE(result.contains("64 hex"));
delete module;
}
// ============================================================================
// 0x prefix handling
// ============================================================================
LOGOS_TEST(wallet_get_balance_accepts_0x_prefix) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("get_balance_value").returns(42);
t.mockCFunction("get_balance_error").returns(0);
QString result = module->wallet_get_balance(VALID_HEX_WITH_PREFIX);
LOGOS_ASSERT_EQ(result, QString("42"));
delete module;
}
// ============================================================================
// Success paths (requires running node + mocked C functions)
// ============================================================================
// Wallet
LOGOS_TEST(wallet_get_balance_returns_balance_string) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("get_balance_value").returns(1000);
t.mockCFunction("get_balance_error").returns(0);
QString result = module->wallet_get_balance(VALID_HEX);
LOGOS_ASSERT_EQ(result, QString("1000"));
LOGOS_ASSERT(t.cFunctionCalled("get_balance"));
delete module;
}
LOGOS_TEST(wallet_get_balance_returns_error_on_ffi_failure) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("get_balance_error").returns(1);
QString result = module->wallet_get_balance(VALID_HEX);
LOGOS_ASSERT_TRUE(result.startsWith("Error:"));
delete module;
}
LOGOS_TEST(wallet_transfer_funds_returns_tx_hash) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("transfer_funds_error").returns(0);
QString result = module->wallet_transfer_funds(VALID_HEX, QStringList{VALID_HEX}, VALID_HEX, "500", "");
LOGOS_ASSERT_FALSE(result.startsWith("Error:"));
LOGOS_ASSERT_EQ(result.length(), 64);
LOGOS_ASSERT_TRUE(result.startsWith("ab"));
LOGOS_ASSERT(t.cFunctionCalled("transfer_funds"));
delete module;
}
LOGOS_TEST(wallet_transfer_funds_with_optional_tip) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("transfer_funds_error").returns(0);
QString result = module->wallet_transfer_funds(VALID_HEX, QStringList{VALID_HEX}, VALID_HEX, "100", VALID_HEX);
LOGOS_ASSERT_FALSE(result.startsWith("Error:"));
LOGOS_ASSERT_EQ(result.length(), 64);
delete module;
}
LOGOS_TEST(wallet_transfer_funds_returns_error_on_ffi_failure) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("transfer_funds_error").returns(1);
QString result = module->wallet_transfer_funds(VALID_HEX, QStringList{VALID_HEX}, VALID_HEX, "100", "");
LOGOS_ASSERT_TRUE(result.startsWith("Error:"));
delete module;
}
LOGOS_TEST(wallet_transfer_funds_single_sender_overload) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("transfer_funds_error").returns(0);
QString result = module->wallet_transfer_funds(VALID_HEX, VALID_HEX, VALID_HEX, "100", "");
LOGOS_ASSERT_FALSE(result.startsWith("Error:"));
LOGOS_ASSERT(t.cFunctionCalled("transfer_funds"));
delete module;
}
LOGOS_TEST(wallet_transfer_funds_multiple_senders) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("transfer_funds_error").returns(0);
QStringList senders;
senders << VALID_HEX << VALID_HEX_WITH_PREFIX.mid(2); // two different addresses
QString result = module->wallet_transfer_funds(VALID_HEX, senders, VALID_HEX, "200", "");
LOGOS_ASSERT_FALSE(result.startsWith("Error:"));
delete module;
}
LOGOS_TEST(wallet_get_known_addresses_returns_addresses) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("get_known_addresses_error").returns(0);
t.mockCFunction("get_known_addresses_count").returns(2);
QStringList addrs = module->wallet_get_known_addresses();
LOGOS_ASSERT_EQ(addrs.size(), 2);
// Mock fills addr0 with 0x11 → hex "1111...11", addr1 with 0x22 → "2222...22"
LOGOS_ASSERT_EQ(addrs[0], QString(64, '1'));
LOGOS_ASSERT_EQ(addrs[1], QString(64, '2'));
LOGOS_ASSERT(t.cFunctionCalled("get_known_addresses"));
LOGOS_ASSERT(t.cFunctionCalled("free_known_addresses"));
delete module;
}
LOGOS_TEST(wallet_get_known_addresses_returns_empty_on_ffi_failure) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("get_known_addresses_error").returns(1);
QStringList addrs = module->wallet_get_known_addresses();
LOGOS_ASSERT_TRUE(addrs.isEmpty());
delete module;
}
// Blend
LOGOS_TEST(blend_join_as_core_node_returns_declaration_id) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("blend_join_as_core_node_error").returns(0);
QStringList locators = {"locator1", "locator2"};
QString result = module->blend_join_as_core_node(VALID_HEX, VALID_HEX, VALID_HEX, locators);
// Mock fills hash with 0xCD → hex "cdcd...cd" (64 chars)
LOGOS_ASSERT_EQ(result.length(), 64);
LOGOS_ASSERT_TRUE(result.startsWith("cd"));
LOGOS_ASSERT(t.cFunctionCalled("blend_join_as_core_node"));
delete module;
}
LOGOS_TEST(blend_join_as_core_node_returns_error_on_ffi_failure) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("blend_join_as_core_node_error").returns(1);
QString result = module->blend_join_as_core_node(VALID_HEX, VALID_HEX, VALID_HEX, {});
LOGOS_ASSERT_TRUE(result.startsWith("Error:"));
delete module;
}
// Explorer
LOGOS_TEST(get_block_returns_json_on_success) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("get_block").returns(R"({"slot":42,"data":"test"})");
t.mockCFunction("get_block_error").returns(0);
QString result = module->get_block(VALID_HEX);
LOGOS_ASSERT_TRUE(result.contains("slot"));
LOGOS_ASSERT_TRUE(result.contains("42"));
LOGOS_ASSERT(t.cFunctionCalled("get_block"));
LOGOS_ASSERT(t.cFunctionCalled("free_cstring"));
delete module;
}
LOGOS_TEST(get_block_returns_error_on_ffi_failure) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("get_block_error").returns(1);
QString result = module->get_block(VALID_HEX);
LOGOS_ASSERT_TRUE(result.startsWith("Error:"));
delete module;
}
LOGOS_TEST(get_blocks_returns_json_on_success) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("get_blocks").returns(R"([{"slot":1},{"slot":2}])");
t.mockCFunction("get_blocks_error").returns(0);
QString result = module->get_blocks(1, 10);
LOGOS_ASSERT_TRUE(result.contains("slot"));
LOGOS_ASSERT(t.cFunctionCalled("get_blocks"));
delete module;
}
LOGOS_TEST(get_blocks_returns_error_on_ffi_failure) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("get_blocks_error").returns(1);
LOGOS_ASSERT_TRUE(module->get_blocks(0, 10).startsWith("Error:"));
delete module;
}
LOGOS_TEST(get_transaction_returns_json_on_success) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("get_transaction").returns(R"({"hash":"abc","status":"confirmed"})");
t.mockCFunction("get_transaction_error").returns(0);
QString result = module->get_transaction(VALID_HEX);
LOGOS_ASSERT_TRUE(result.contains("confirmed"));
LOGOS_ASSERT(t.cFunctionCalled("get_transaction"));
LOGOS_ASSERT(t.cFunctionCalled("free_cstring"));
delete module;
}
LOGOS_TEST(get_transaction_returns_error_on_ffi_failure) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("get_transaction_error").returns(1);
LOGOS_ASSERT_TRUE(module->get_transaction(VALID_HEX).startsWith("Error:"));
delete module;
}
// Cryptarchia
LOGOS_TEST(get_cryptarchia_info_returns_json_on_success) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("get_cryptarchia_info_error").returns(0);
t.mockCFunction("cryptarchia_slot").returns(100);
t.mockCFunction("cryptarchia_height").returns(50);
t.mockCFunction("cryptarchia_mode").returns(1); // Online
QString result = module->get_cryptarchia_info();
LOGOS_ASSERT_FALSE(result.startsWith("Error:"));
LOGOS_ASSERT_TRUE(result.contains("slot"));
LOGOS_ASSERT_TRUE(result.contains("100"));
LOGOS_ASSERT_TRUE(result.contains("height"));
LOGOS_ASSERT_TRUE(result.contains("50"));
LOGOS_ASSERT_TRUE(result.contains("Online"));
LOGOS_ASSERT_TRUE(result.contains("lib"));
LOGOS_ASSERT_TRUE(result.contains("tip"));
LOGOS_ASSERT(t.cFunctionCalled("get_cryptarchia_info"));
LOGOS_ASSERT(t.cFunctionCalled("free_cryptarchia_info"));
delete module;
}
LOGOS_TEST(get_cryptarchia_info_bootstrapping_mode) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("get_cryptarchia_info_error").returns(0);
t.mockCFunction("cryptarchia_mode").returns(0); // Bootstrapping
QString result = module->get_cryptarchia_info();
LOGOS_ASSERT_TRUE(result.contains("Bootstrapping"));
delete module;
}
LOGOS_TEST(get_cryptarchia_info_returns_error_on_ffi_failure) {
auto t = LogosTestContext("blockchain_module");
QTemporaryDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("get_cryptarchia_info_error").returns(1);
LOGOS_ASSERT_TRUE(module->get_cryptarchia_info().startsWith("Error:"));
delete module;
}