Merge 6b6a640998a7ba2c045e6ba8dd241c0e13fdafdf into 08bd849d5a4d331d7c206cdd291105c95147dc12

This commit is contained in:
Iuri Matias 2026-05-08 19:49:30 +00:00 committed by GitHub
commit fc4c691cda
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 3053 additions and 1242 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,18 @@
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}"
# Universal module generated_code/ is picked up automatically by LogosModule.cmake
logos_module(
NAME liblogos_blockchain_module
SOURCES
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

@ -4,17 +4,34 @@
"description": "Logos blockchain node for logos-core",
"author": "Logos Blockchain Team",
"type": "core",
"interface": "universal",
"codegen": {
"impl_header": "logos_blockchain_module.h"
},
"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": ["nlohmann_json", "boost"]
},
"external_libraries": [
{ "name": "logos_blockchain" }
],
"cmake": {
"extra_include_dirs": ["lib"]
}
}
}

View File

@ -1,53 +0,0 @@
#ifndef I_LOGOS_BLOCKCHAIN_MODULE_API_H
#define I_LOGOS_BLOCKCHAIN_MODULE_API_H
#include <QString>
#include <core/interface.h>
class ILogosBlockchainModule {
public:
virtual ~ILogosBlockchainModule() = default;
// Logos Core
virtual void initLogos(LogosAPI* logos_api_instance) = 0;
// ---- Node ----
// Lifecycle
virtual int generate_user_config(const QVariantMap& args) = 0;
virtual int generate_user_config_from_str(const QString& args) = 0;
virtual int start(const QString& config_path, const QString& deployment) = 0;
virtual int stop() = 0;
// Wallet
virtual QString wallet_get_balance(const QString& address_hex) = 0;
virtual QString wallet_transfer_funds(
const QString& change_public_key,
const QStringList& sender_addresses,
const QString& recipient_address,
const QString& amount,
const QString& optional_tip_hex
) = 0;
virtual QStringList wallet_get_known_addresses() = 0;
// Blend
virtual QString blend_join_as_core_node(
const QString& provider_id_hex,
const QString& zk_id_hex,
const QString& locked_note_id_hex,
const QStringList& locators
) = 0;
// Storage
virtual QString get_block(const QString& header_id_hex) = 0;
virtual QString get_blocks(quint64 from_slot, quint64 to_slot) = 0;
virtual QString get_transaction(const QString& tx_hash_hex) = 0;
// Cryptarchia
virtual QString get_cryptarchia_info() = 0;
};
#define ILogosBlockchainModule_iid "org.logos.ilogosblockchainmodule"
Q_DECLARE_INTERFACE(ILogosBlockchainModule, ILogosBlockchainModule_iid)
#endif

View File

@ -1,62 +1,86 @@
#include "logos_blockchain_module.h"
#include "logos_api_client.h"
#include <QByteArray>
#include <QDir>
#include <QJsonDocument>
#include <QJsonObject>
#include <QVariant>
#include <boost/algorithm/hex.hpp>
#include <boost/algorithm/string/trim.hpp>
#include <charconv>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
namespace fs = std::filesystem;
using json = nlohmann::json;
// 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.
std::string localPathFromFileUrl(const std::string& s) {
if (s.size() >= 7 && s.substr(0, 7) == "file://") return s.substr(7);
if (s.size() >= 5 && s.substr(0, 5) == "file:") return s.substr(5);
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;
// Parses ADDRESS_HEX_LEN hex chars (optional 0x prefix) to ADDRESS_BYTES. Returns empty QByteArray on error.
QByteArray parse_address_hex(const QString& address_hex) {
QString hex = address_hex.trimmed();
if (hex.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive))
hex = hex.mid(2);
if (hex.length() != ADDRESS_HEX_LEN)
std::vector<uint8_t> parse_address_hex(const std::string& address_hex) {
std::string hex = address_hex;
boost::algorithm::trim(hex);
if (hex.size() >= 2 && hex[0] == '0' && (hex[1] == 'x' || hex[1] == 'X'))
hex = hex.substr(2);
if (static_cast<int>(hex.size()) != ADDRESS_HEX_LEN)
return {};
QByteArray bytes = QByteArray::fromHex(hex.toUtf8());
if (bytes.size() != ADDRESS_BYTES)
try {
std::string decoded;
boost::algorithm::unhex(hex.begin(), hex.end(), std::back_inserter(decoded));
return {decoded.begin(), decoded.end()};
} catch (const boost::algorithm::non_hex_input&) {
return {};
return bytes;
}
}
std::string bytes_to_hex(const uint8_t* data, size_t len) {
std::string out;
out.reserve(len * 2);
boost::algorithm::hex_lower(data, data + len, std::back_inserter(out));
return out;
}
// Wrapper that owns data and provides GenerateConfigArgs
struct OwnedGenerateConfigArgs {
std::vector<QByteArray> initial_peers_data;
std::vector<std::string> initial_peers_data;
std::vector<const char*> initial_peers_ptrs;
uint32_t initial_peers_count_val;
QByteArray output_data;
std::string output_data;
uint16_t net_port_val;
uint16_t blend_port_val;
QByteArray http_addr_data;
QByteArray external_address_data;
std::string http_addr_data;
std::string external_address_data;
bool no_public_ip_check_val;
QByteArray custom_deployment_config_path_data;
std::string custom_deployment_config_path_data;
Deployment deployment_val{};
QByteArray state_path_data;
std::string state_path_data;
// The FFI struct with pointers into owned data
GenerateConfigArgs ffi_args{};
// Constructor that populates both owned data and FFI struct
explicit OwnedGenerateConfigArgs(const QVariantMap& args) {
// initial_peers (QStringList -> const char**)
if (args.contains("initial_peers")) {
QStringList peers = args["initial_peers"].toStringList();
initial_peers_count_val = static_cast<uint32_t>(peers.size());
for (const QString& peer : peers) {
initial_peers_data.push_back(peer.toUtf8());
// Constructor that populates both owned data and FFI struct from JSON
explicit OwnedGenerateConfigArgs(const json& args) {
// initial_peers (JSON array -> const char**)
if (args.contains("initial_peers") && args["initial_peers"].is_array()) {
for (const auto& peer : args["initial_peers"]) {
initial_peers_data.push_back(peer.get<std::string>());
}
for (const QByteArray& data : initial_peers_data) {
initial_peers_ptrs.push_back(data.constData());
initial_peers_count_val = static_cast<uint32_t>(initial_peers_data.size());
for (const std::string& data : initial_peers_data) {
initial_peers_ptrs.push_back(data.c_str());
}
ffi_args.initial_peers = initial_peers_ptrs.data();
@ -66,49 +90,49 @@ namespace {
ffi_args.initial_peers_count = nullptr;
}
// output (QString -> const char*)
if (args.contains("output")) {
output_data = args["output"].toString().toUtf8();
ffi_args.output = output_data.constData();
// output (string -> const char*)
if (args.contains("output") && args["output"].is_string()) {
output_data = args["output"].get<std::string>();
ffi_args.output = output_data.c_str();
} else {
ffi_args.output = nullptr;
}
// net_port (int -> const uint16_t*)
if (args.contains("net_port")) {
net_port_val = static_cast<uint16_t>(args["net_port"].toInt());
if (args.contains("net_port") && args["net_port"].is_number_integer()) {
net_port_val = static_cast<uint16_t>(args["net_port"].get<int>());
ffi_args.net_port = &net_port_val;
} else {
ffi_args.net_port = nullptr;
}
// blend_port (int -> const uint16_t*)
if (args.contains("blend_port")) {
blend_port_val = static_cast<uint16_t>(args["blend_port"].toInt());
if (args.contains("blend_port") && args["blend_port"].is_number_integer()) {
blend_port_val = static_cast<uint16_t>(args["blend_port"].get<int>());
ffi_args.blend_port = &blend_port_val;
} else {
ffi_args.blend_port = nullptr;
}
// http_addr (QString -> const char*)
if (args.contains("http_addr")) {
http_addr_data = args["http_addr"].toString().toUtf8();
ffi_args.http_addr = http_addr_data.constData();
// http_addr (string -> const char*)
if (args.contains("http_addr") && args["http_addr"].is_string()) {
http_addr_data = args["http_addr"].get<std::string>();
ffi_args.http_addr = http_addr_data.c_str();
} else {
ffi_args.http_addr = nullptr;
}
// external_address (QString -> const char*)
if (args.contains("external_address")) {
external_address_data = args["external_address"].toString().toUtf8();
ffi_args.external_address = external_address_data.constData();
// external_address (string -> const char*)
if (args.contains("external_address") && args["external_address"].is_string()) {
external_address_data = args["external_address"].get<std::string>();
ffi_args.external_address = external_address_data.c_str();
} else {
ffi_args.external_address = nullptr;
}
// no_public_ip_check (bool -> const bool*)
if (args.contains("no_public_ip_check")) {
no_public_ip_check_val = args["no_public_ip_check"].toBool();
if (args.contains("no_public_ip_check") && args["no_public_ip_check"].is_boolean()) {
no_public_ip_check_val = args["no_public_ip_check"].get<bool>();
ffi_args.no_public_ip_check = &no_public_ip_check_val;
} else {
ffi_args.no_public_ip_check = nullptr;
@ -117,22 +141,21 @@ namespace {
// deployment (const struct Deployment*)
// Expected format: { "deployment": { "well_known_deployment": "devnet" } }
// OR: { "deployment": { "config_path": "/path/to/config" } }
if (args.contains("deployment")) {
const QVariantMap deployment = args["deployment"].toMap(); // NOLINT: Move definition to if-statement
if (args.contains("deployment") && args["deployment"].is_object()) {
const auto& deployment = args["deployment"];
if (deployment.contains("well_known_deployment")) {
if (deployment.contains("well_known_deployment") && deployment["well_known_deployment"].is_string()) {
deployment_val.deployment_type = DeploymentType::WellKnown;
const QString wellknown =
deployment["well_known_deployment"].toString(); // NOLINT: Move definition to if-statement
const std::string wellknown = deployment["well_known_deployment"].get<std::string>();
if (wellknown == "devnet") {
deployment_val.well_known_deployment = WellKnownDeployment::Devnet;
}
deployment_val.custom_deployment_config_path = nullptr;
} else if (deployment.contains("config_path")) {
} else if (deployment.contains("config_path") && deployment["config_path"].is_string()) {
deployment_val.deployment_type = DeploymentType::Custom;
deployment_val.well_known_deployment = static_cast<WellKnownDeployment>(0);
custom_deployment_config_path_data = deployment["config_path"].toString().toUtf8();
deployment_val.custom_deployment_config_path = custom_deployment_config_path_data.constData();
custom_deployment_config_path_data = deployment["config_path"].get<std::string>();
deployment_val.custom_deployment_config_path = custom_deployment_config_path_data.c_str();
}
ffi_args.deployment = &deployment_val;
@ -140,10 +163,10 @@ namespace {
ffi_args.deployment = nullptr;
}
// state_path (QString -> const char*)
if (args.contains("state_path")) {
state_path_data = args["state_path"].toString().toUtf8();
ffi_args.state_path = state_path_data.constData();
// state_path (string -> const char*)
if (args.contains("state_path") && args["state_path"].is_string()) {
state_path_data = args["state_path"].get<std::string>();
ffi_args.state_path = state_path_data.c_str();
} else {
ffi_args.state_path = nullptr;
}
@ -154,35 +177,38 @@ namespace {
namespace environment {
constexpr auto LOGOS_BLOCKCHAIN_CIRCUITS = "LOGOS_BLOCKCHAIN_CIRCUITS";
// Checks the directory exists and ensures it contains at least one file to avoid an empty directory false positive
bool is_circuits_path_valid(const QString& path) {
const QDir directory(path);
return directory.exists() && !directory.entryList(QDir::Files | QDir::NoDotAndDotDot).isEmpty();
bool is_circuits_path_valid(const std::string& path) {
std::error_code ec;
if (!fs::is_directory(path, ec)) return false;
for (const auto& entry : fs::directory_iterator(path, ec)) {
if (entry.is_regular_file()) return true;
}
return false;
}
void setup_circuits_path(const LogosAPI& logos_api) {
const QString module_path = logos_api.property("modulePath").toString();
const QDir module_directory(module_path);
void setup_circuits_path(const std::string& module_path) {
fs::path circuits_path = fs::path(module_path) / "circuits";
std::string circuits_str = circuits_path.string();
const QString circuits_path = module_directory.filePath(QStringLiteral("circuits"));
if (!is_circuits_path_valid(circuits_path)) {
qFatal() << "The LOGOS_BLOCKCHAIN_CIRCUITS environment variable is not set or does not contain any files.";
if (!is_circuits_path_valid(circuits_str)) {
fprintf(stderr, "FATAL: The LOGOS_BLOCKCHAIN_CIRCUITS environment variable is not set or does not contain any files.\n");
return;
}
qputenv("LOGOS_BLOCKCHAIN_CIRCUITS", circuits_path.toUtf8());
qInfo() << "LOGOS_BLOCKCHAIN_CIRCUITS set to:" << circuits_path;
setenv("LOGOS_BLOCKCHAIN_CIRCUITS", circuits_str.c_str(), 1);
fprintf(stderr, "LOGOS_BLOCKCHAIN_CIRCUITS set to: %s\n", circuits_str.c_str());
}
} // namespace environment
void LogosBlockchainModule::on_new_block_callback(const char* block) {
if (s_instance) {
qInfo() << "Received new block: " << block;
QVariantList data;
data.append(QString::fromUtf8(block));
s_instance->emit_event("newBlock", data);
fprintf(stderr, "Received new block: %s\n", block);
json j;
j["block"] = std::string(block);
if (s_instance->emitEvent)
s_instance->emitEvent("newBlock", j.dump());
// SAFETY:
// We are getting an owned pointer here which is freed after this callback is called, so there is not need to
// We are getting an owned pointer here which is freed after this callback is called, so there is no need to
// free the resource here as we are copying the data!
}
}
@ -198,100 +224,81 @@ LogosBlockchainModule::~LogosBlockchainModule() {
}
}
// Logos Core
QString LogosBlockchainModule::name() const {
return "liblogos_blockchain_module";
}
QString LogosBlockchainModule::version() const {
return "1.0.0";
}
void LogosBlockchainModule::initLogos(LogosAPI* logos_api_instance) {
logosAPI = logos_api_instance;
if (logosAPI) {
client = logosAPI->getClient("liblogos_blockchain_module");
if (!client) {
qWarning() << "LogosBlockchainModule: Failed to get liblogos_blockchain_module client";
}
}
}
// ---- Node ----
// Lifecycle
int LogosBlockchainModule::generate_user_config(const QVariantMap& args) {
const OwnedGenerateConfigArgs owned_args(args);
int LogosBlockchainModule::generate_user_config(const std::string& json_args) {
json parsed_args;
try {
parsed_args = json::parse(json_args);
} catch (const json::parse_error& e) {
fprintf(stderr, "Failed to parse JSON args: %s\n", e.what());
return 1;
}
const OwnedGenerateConfigArgs owned_args(parsed_args);
const OperationStatus status = ::generate_user_config(owned_args.ffi_args);
if (!is_ok(&status)) {
qCritical() << "Failed to generate user config. Error:" << status;
fprintf(stderr, "Failed to generate user config. Error: %d\n", status);
return 1;
}
return 0;
}
int LogosBlockchainModule::generate_user_config_from_str(const QString& args) {
const QVariantMap parsed_args = QJsonDocument::fromJson(args.toUtf8()).object().toVariantMap();
return generate_user_config(parsed_args);
}
int LogosBlockchainModule::start(const QString& config_path, const QString& deployment) {
int LogosBlockchainModule::start(const std::string& config_path, const std::string& deployment) {
if (node) {
qWarning() << "Could not execute the operation: The node is already running.";
fprintf(stderr, "Could not execute the operation: The node is already running.\n");
return 1;
}
if (!logosAPI) {
qCritical() << "LogosAPI instance is null, cannot start node.";
return 2;
const char* module_path_env = std::getenv("LOGOS_MODULE_PATH");
if (module_path_env && *module_path_env) {
environment::setup_circuits_path(module_path_env);
} else {
fprintf(stderr, "Warning: LOGOS_MODULE_PATH not set, skipping circuits path setup.\n");
}
environment::setup_circuits_path(*logosAPI);
QString effective_config_path = config_path;
std::string effective_config_path = config_path;
if (effective_config_path.isEmpty()) {
const char* env = std::getenv("LB_CONFIG_PATH"); // NOLINT: Move definition to if-statement
if (effective_config_path.empty()) {
const char* env = std::getenv("LB_CONFIG_PATH");
if (env && *env) {
effective_config_path = QString::fromUtf8(env);
qInfo() << "Using config from LB_CONFIG_PATH:" << effective_config_path;
effective_config_path = env;
fprintf(stderr, "Using config from LB_CONFIG_PATH: %s\n", effective_config_path.c_str());
} else {
qCritical() << "Config path was not specified and LB_CONFIG_PATH is not set.";
fprintf(stderr, "Config path was not specified and LB_CONFIG_PATH is not set.\n");
return 3;
}
}
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 std::string 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 char* config_path_ptr = effective_config_path.empty() ? nullptr : effective_config_path.c_str();
const char* deployment_ptr = deployment_path.empty() ? nullptr : deployment_path.c_str();
auto [value, error] = start_lb_node(config_path_ptr, deployment_ptr);
qInfo() << "Start node returned with value and error.";
fprintf(stderr, "Start node returned with value and error.\n");
if (!is_ok(&error)) {
qCritical() << "Failed to start the node. Error:" << error;
fprintf(stderr, "Failed to start the node. Error: %d\n", error);
return 4;
}
node = value;
qInfo() << "The node was started successfully.";
fprintf(stderr, "The node was started successfully.\n");
// Subscribe to block events
if (!node) {
qWarning() << "Could not subscribe to block events: The node is not running.";
fprintf(stderr, "Could not subscribe to block events: The node is not running.\n");
return 4;
}
s_instance = this;
const OperationStatus subscribe_status = subscribe_to_new_blocks(node, on_new_block_callback);
if (!is_ok(&subscribe_status)) {
qCritical() << "Failed to subscribe to new blocks. Error:" << subscribe_status;
fprintf(stderr, "Failed to subscribe to new blocks. Error: %d\n", subscribe_status);
return 5;
}
@ -300,17 +307,17 @@ int LogosBlockchainModule::start(const QString& config_path, const QString& depl
int LogosBlockchainModule::stop() {
if (!node) {
qWarning() << "Could not execute the operation: The node is not running.";
fprintf(stderr, "Could not execute the operation: The node is not running.\n");
return 1;
}
s_instance = nullptr; // Clear before stopping to prevent callbacks during shutdown
s_instance = nullptr;
const OperationStatus status = stop_node(node);
if (is_ok(&status)) {
qInfo() << "The node was stopped successfully.";
fprintf(stderr, "The node was stopped successfully.\n");
} else {
qCritical() << "Could not stop the node. Error:" << status;
fprintf(stderr, "Could not stop the node. Error: %d\n", status);
}
node = nullptr;
@ -319,296 +326,262 @@ int LogosBlockchainModule::stop() {
// Wallet
QString LogosBlockchainModule::wallet_get_balance(const QString& address_hex) {
qDebug() << "wallet_get_balance: address_hex=" << address_hex;
std::string LogosBlockchainModule::wallet_get_balance(const std::string& address_hex) {
fprintf(stderr, "wallet_get_balance: address_hex=%s\n", address_hex.c_str());
if (!node) {
return QStringLiteral("Error: The node is not running.");
return "Error: The node is not running.";
}
const QByteArray bytes = parse_address_hex(address_hex);
if (bytes.isEmpty() || bytes.size() != ADDRESS_BYTES) {
return QStringLiteral("Error: Address must be 64 hex characters (32 bytes).");
const std::vector<uint8_t> bytes = parse_address_hex(address_hex);
if (bytes.empty() || static_cast<int>(bytes.size()) != ADDRESS_BYTES) {
return "Error: Address must be 64 hex characters (32 bytes).";
}
auto [value, error] = get_balance(node, reinterpret_cast<const uint8_t*>(bytes.constData()), nullptr);
auto [value, error] = get_balance(node, bytes.data(), nullptr);
if (!is_ok(&error)) {
return QStringLiteral("Error: Failed to get balance: ") + QString::number(error);
return "Error: Failed to get balance: " + std::to_string(error);
}
return QString::number(value);
return std::to_string(value);
}
QString LogosBlockchainModule::wallet_transfer_funds(
const QString& change_public_key,
const QStringList& sender_addresses,
const QString& recipient_address,
const QString& amount,
const QString& optional_tip_hex
std::string LogosBlockchainModule::wallet_transfer_funds(
const std::string& change_public_key,
const std::vector<std::string>& sender_addresses,
const std::string& recipient_address,
const std::string& amount,
const std::string& optional_tip_hex
) {
if (!node) {
return QStringLiteral("Error: The node is not running.");
return "Error: The node is not running.";
}
bool ok = false;
const quint64 amount_val = amount.trimmed().toULongLong(&ok);
if (!ok) {
return QStringLiteral("Error: Invalid amount (positive integer required).");
std::string amount_trimmed = amount;
boost::algorithm::trim(amount_trimmed);
uint64_t amount_val = 0;
auto [ptr, ec] = std::from_chars(amount_trimmed.data(), amount_trimmed.data() + amount_trimmed.size(), amount_val);
if (ec != std::errc{} || ptr != amount_trimmed.data() + amount_trimmed.size() || amount_trimmed.empty()) {
return "Error: Invalid amount (positive integer required).";
}
const QByteArray change_bytes = parse_address_hex(change_public_key);
if (change_bytes.isEmpty() || change_bytes.size() != ADDRESS_BYTES) {
return QStringLiteral("Error: Invalid change_public_key (64 hex characters required).");
const std::vector<uint8_t> change_bytes = parse_address_hex(change_public_key);
if (change_bytes.empty() || static_cast<int>(change_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid change_public_key (64 hex characters required).";
}
const QByteArray recipient_bytes = parse_address_hex(recipient_address);
if (recipient_bytes.isEmpty() || recipient_bytes.size() != ADDRESS_BYTES) {
return QStringLiteral("Error: Invalid recipient_address (64 hex characters required).");
const std::vector<uint8_t> recipient_bytes = parse_address_hex(recipient_address);
if (recipient_bytes.empty() || static_cast<int>(recipient_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid recipient_address (64 hex characters required).";
}
if (sender_addresses.isEmpty()) {
return QStringLiteral("Error: At least one sender address required.");
if (sender_addresses.empty()) {
return "Error: At least one sender address required.";
}
QVector<QByteArray> funding_bytes;
for (const QString& hex : sender_addresses) {
QByteArray b = parse_address_hex(hex);
if (b.isEmpty() || b.size() != ADDRESS_BYTES) {
return QStringLiteral("Error: Invalid sender address (64 hex characters required).");
std::vector<std::vector<uint8_t>> funding_bytes;
for (const std::string& hex : sender_addresses) {
std::vector<uint8_t> b = parse_address_hex(hex);
if (b.empty() || static_cast<int>(b.size()) != ADDRESS_BYTES) {
return "Error: Invalid sender address (64 hex characters required).";
}
funding_bytes.append(b);
funding_bytes.push_back(std::move(b));
}
QVector<const uint8_t*> funding_ptrs;
for (const QByteArray& b : funding_bytes)
funding_ptrs.append(reinterpret_cast<const uint8_t*>(b.constData()));
std::vector<const uint8_t*> funding_ptrs;
for (const auto& b : funding_bytes)
funding_ptrs.push_back(b.data());
QByteArray tip_bytes; // NOLINT: Needs to be outside of scope for lifetime
std::vector<uint8_t> tip_bytes;
const HeaderId* optional_tip = nullptr;
if (!optional_tip_hex.isEmpty()) {
if (!optional_tip_hex.empty()) {
tip_bytes = parse_address_hex(optional_tip_hex);
if (tip_bytes.isEmpty() || tip_bytes.size() != ADDRESS_BYTES) {
return QStringLiteral("Error: Invalid optional tip (64 hex characters or empty).");
if (tip_bytes.empty() || static_cast<int>(tip_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid optional tip (64 hex characters or empty).";
}
optional_tip = reinterpret_cast<const HeaderId*>(tip_bytes.constData());
optional_tip = reinterpret_cast<const HeaderId*>(tip_bytes.data());
}
TransferFundsArguments args{};
args.optional_tip = optional_tip;
args.change_public_key = reinterpret_cast<const uint8_t*>(change_bytes.constData());
args.funding_public_keys = funding_ptrs.constData();
args.funding_public_keys_len = static_cast<size_t>(funding_ptrs.size());
args.recipient_public_key = reinterpret_cast<const uint8_t*>(recipient_bytes.constData());
args.change_public_key = change_bytes.data();
args.funding_public_keys = funding_ptrs.data();
args.funding_public_keys_len = funding_ptrs.size();
args.recipient_public_key = recipient_bytes.data();
args.amount = amount_val;
auto [value, error] = transfer_funds(node, &args);
if (!is_ok(&error)) {
return QStringLiteral("Error: Failed to transfer funds: ") + QString::number(error);
return "Error: Failed to transfer funds: " + std::to_string(error);
}
// value is Hash (32 bytes); convert to hex string
const QByteArray hash_bytes(reinterpret_cast<const char*>(&value), ADDRESS_BYTES);
return QString::fromUtf8(hash_bytes.toHex());
return bytes_to_hex(reinterpret_cast<const uint8_t*>(&value), ADDRESS_BYTES);
}
QString LogosBlockchainModule::wallet_transfer_funds(
const QString& change_public_key,
const QString& sender_address,
const QString& recipient_address,
const QString& amount,
const QString& optional_tip_hex
) {
return wallet_transfer_funds(
change_public_key, QStringList{sender_address}, recipient_address, amount, optional_tip_hex
);
}
QStringList LogosBlockchainModule::wallet_get_known_addresses() {
QStringList out;
std::vector<std::string> LogosBlockchainModule::wallet_get_known_addresses() {
std::vector<std::string> out;
if (!node) {
qWarning() << "Could not execute the operation: The node is not running.";
fprintf(stderr, "Could not execute the operation: The node is not running.\n");
return out;
}
auto [value, error] = get_known_addresses(node);
if (!is_ok(&error)) {
qCritical() << "Failed to get known addresses. Error:" << error;
fprintf(stderr, "Failed to get known addresses. Error: %d\n", error);
return out;
}
// Each address is ADDRESS_BYTES (32) bytes
for (size_t i = 0; i < value.len; ++i) {
const uint8_t* ptr = value.addresses[i]; // NOLINT: Move definition to if-statement
const uint8_t* ptr = value.addresses[i];
if (ptr) {
QByteArray addr(reinterpret_cast<const char*>(ptr), ADDRESS_BYTES);
out.append(QString::fromUtf8(addr.toHex()));
out.push_back(bytes_to_hex(ptr, ADDRESS_BYTES));
}
}
const OperationStatus free_status = free_known_addresses(value);
if (!is_ok(&free_status)) {
qWarning() << "Failed to free known addresses. Error:" << free_status;
fprintf(stderr, "Failed to free known addresses. Error: %d\n", free_status);
}
qDebug() << "blockchain lib: known addresses, count=" << out.size()
<< "sample:" << (out.isEmpty() ? QLatin1String("(none)") : out.constFirst());
fprintf(stderr, "blockchain lib: known addresses, count=%zu sample:%s\n",
out.size(), out.empty() ? "(none)" : out.front().c_str());
return out;
}
// Blend
QString LogosBlockchainModule::blend_join_as_core_node(
const QString& provider_id_hex,
const QString& zk_id_hex,
const QString& locked_note_id_hex,
const QStringList& locators
std::string LogosBlockchainModule::blend_join_as_core_node(
const std::string& provider_id_hex,
const std::string& zk_id_hex,
const std::string& locked_note_id_hex,
const std::vector<std::string>& locators
) {
if (!node) {
return QStringLiteral("Error: The node is not running.");
return "Error: The node is not running.";
}
const QByteArray provider_id_bytes = parse_address_hex(provider_id_hex);
if (provider_id_bytes.isEmpty() || provider_id_bytes.size() != ADDRESS_BYTES) {
return QStringLiteral("Error: Invalid provider_id_hex (64 hex characters required).");
const std::vector<uint8_t> provider_id_bytes = parse_address_hex(provider_id_hex);
if (provider_id_bytes.empty() || static_cast<int>(provider_id_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid provider_id_hex (64 hex characters required).";
}
const QByteArray zk_id_bytes = parse_address_hex(zk_id_hex);
if (zk_id_bytes.isEmpty() || zk_id_bytes.size() != ADDRESS_BYTES) {
return QStringLiteral("Error: Invalid zk_id_hex (64 hex characters required).");
const std::vector<uint8_t> zk_id_bytes = parse_address_hex(zk_id_hex);
if (zk_id_bytes.empty() || static_cast<int>(zk_id_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid zk_id_hex (64 hex characters required).";
}
const QByteArray locked_note_id_bytes = parse_address_hex(locked_note_id_hex);
if (locked_note_id_bytes.isEmpty() || locked_note_id_bytes.size() != ADDRESS_BYTES) {
return QStringLiteral("Error: Invalid locked_note_id_hex (64 hex characters required).");
const std::vector<uint8_t> locked_note_id_bytes = parse_address_hex(locked_note_id_hex);
if (locked_note_id_bytes.empty() || static_cast<int>(locked_note_id_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid locked_note_id_hex (64 hex characters required).";
}
// QString is UTF-16, but the FFI requires UTF-8.
// locators_data owns the converted buffers, while locators_ptrs holds raw pointers into them for the FFI call.
// Using reserve() prevents reallocation, keeping the constData() pointers stable.
std::vector<QByteArray> locators_data;
// locators_ptrs holds raw pointers into the std::strings (valid as long as locators lives).
std::vector<const char*> locators_ptrs;
locators_data.reserve(locators.size());
locators_ptrs.reserve(locators.size());
for (const QString& locator : locators) {
locators_data.push_back(locator.toUtf8());
locators_ptrs.push_back(locators_data.back().constData());
for (const std::string& locator : locators) {
locators_ptrs.push_back(locator.c_str());
}
auto [value, error] = ::blend_join_as_core_node(
node,
reinterpret_cast<const uint8_t*>(provider_id_bytes.constData()),
reinterpret_cast<const uint8_t*>(zk_id_bytes.constData()),
reinterpret_cast<const uint8_t*>(locked_note_id_bytes.constData()),
provider_id_bytes.data(),
zk_id_bytes.data(),
locked_note_id_bytes.data(),
locators_ptrs.data(),
locators_ptrs.size()
);
if (!is_ok(&error)) {
return QStringLiteral("Error: Failed to join as core node: ") + QString::number(error);
return "Error: Failed to join as core node: " + std::to_string(error);
}
const QByteArray declaration_id_bytes(reinterpret_cast<const char*>(&value), sizeof(value));
const QString declaration_id = QString::fromUtf8(declaration_id_bytes.toHex());
qInfo() << "Successfully joined as core node. DeclarationId:" << declaration_id;
std::string declaration_id = bytes_to_hex(reinterpret_cast<const uint8_t*>(&value), sizeof(value));
fprintf(stderr, "Successfully joined as core node. DeclarationId: %s\n", declaration_id.c_str());
return declaration_id;
}
// Storage
// Explorer
QString LogosBlockchainModule::get_block(const QString& header_id_hex) {
std::string LogosBlockchainModule::get_block(const std::string& header_id_hex) {
if (!node) {
return QStringLiteral("Error: The node is not running.");
return "Error: The node is not running.";
}
const QByteArray bytes = parse_address_hex(header_id_hex);
if (bytes.isEmpty() || bytes.size() != ADDRESS_BYTES) {
return QStringLiteral("Error: Header ID must be 64 hex characters (32 bytes).");
const std::vector<uint8_t> bytes = parse_address_hex(header_id_hex);
if (bytes.empty() || static_cast<int>(bytes.size()) != ADDRESS_BYTES) {
return "Error: Header ID must be 64 hex characters (32 bytes).";
}
auto [value, error] = ::get_block(node, reinterpret_cast<const HeaderId*>(bytes.constData()));
auto [value, error] = ::get_block(node, reinterpret_cast<const HeaderId*>(bytes.data()));
if (!is_ok(&error)) {
qWarning() << "Failed to get block. Error:" << error;
return QStringLiteral("Error: Failed to get block: ") + QString::number(error);
fprintf(stderr, "Failed to get block. Error: %d\n", error);
return "Error: Failed to get block: " + std::to_string(error);
}
const QString result = QString::fromUtf8(value);
std::string result(value);
const OperationStatus free_status = free_cstring(value);
if (!is_ok(&free_status)) {
qWarning() << "Failed to free block string. Error:" << free_status;
fprintf(stderr, "Failed to free block string. Error: %d\n", free_status);
}
return result;
}
QString LogosBlockchainModule::get_blocks(const quint64 from_slot, const quint64 to_slot) {
std::string LogosBlockchainModule::get_blocks(const uint64_t from_slot, const uint64_t to_slot) {
if (!node) {
return QStringLiteral("Error: The node is not running.");
return "Error: The node is not running.";
}
auto [value, error] = ::get_blocks(node, from_slot, to_slot);
if (!is_ok(&error)) {
qWarning() << "Failed to get blocks. Error:" << error;
return QStringLiteral("Error: Failed to get blocks: ") + QString::number(error);
fprintf(stderr, "Failed to get blocks. Error: %d\n", error);
return "Error: Failed to get blocks: " + std::to_string(error);
}
const QString result = QString::fromUtf8(value);
std::string result(value);
const OperationStatus free_status = free_cstring(value);
if (!is_ok(&free_status)) {
qWarning() << "Failed to free blocks string. Error:" << free_status;
fprintf(stderr, "Failed to free blocks string. Error: %d\n", free_status);
}
return result;
}
QString LogosBlockchainModule::get_transaction(const QString& tx_hash_hex) {
std::string LogosBlockchainModule::get_transaction(const std::string& tx_hash_hex) {
if (!node) {
return QStringLiteral("Error: The node is not running.");
return "Error: The node is not running.";
}
const QByteArray bytes = parse_address_hex(tx_hash_hex);
if (bytes.isEmpty() || bytes.size() != ADDRESS_BYTES) {
return QStringLiteral("Error: Transaction hash must be 64 hex characters (32 bytes).");
const std::vector<uint8_t> bytes = parse_address_hex(tx_hash_hex);
if (bytes.empty() || static_cast<int>(bytes.size()) != ADDRESS_BYTES) {
return "Error: Transaction hash must be 64 hex characters (32 bytes).";
}
auto [value, error] = ::get_transaction(node, reinterpret_cast<const TxHash*>(bytes.constData()));
auto [value, error] = ::get_transaction(node, reinterpret_cast<const TxHash*>(bytes.data()));
if (!is_ok(&error)) {
qWarning() << "Failed to get transaction. Error:" << error;
return QStringLiteral("Error: Failed to get transaction: ") + QString::number(error);
fprintf(stderr, "Failed to get transaction. Error: %d\n", error);
return "Error: Failed to get transaction: " + std::to_string(error);
}
const QString result = QString::fromUtf8(value);
std::string result(value);
const OperationStatus free_status = free_cstring(value);
if (!is_ok(&free_status)) {
qWarning() << "Failed to free transaction string. Error:" << free_status;
fprintf(stderr, "Failed to free transaction string. Error: %d\n", free_status);
}
return result;
}
// Cryptarchia
QString LogosBlockchainModule::get_cryptarchia_info() {
std::string LogosBlockchainModule::get_cryptarchia_info() {
if (!node) {
return QStringLiteral("Error: The node is not running.");
return "Error: The node is not running.";
}
auto [value, error] = ::get_cryptarchia_info(node);
if (!is_ok(&error)) {
qWarning() << "Failed to get cryptarchia info. Error:" << error;
return QStringLiteral("Error: Failed to get cryptarchia info: ") + QString::number(error);
fprintf(stderr, "Failed to get cryptarchia info. Error: %d\n", error);
return "Error: Failed to get cryptarchia info: " + std::to_string(error);
}
QJsonObject obj;
obj[QStringLiteral("lib")] =
QString::fromUtf8(QByteArray(reinterpret_cast<const char*>(value->lib), ADDRESS_BYTES).toHex());
obj[QStringLiteral("tip")] =
QString::fromUtf8(QByteArray(reinterpret_cast<const char*>(value->tip), ADDRESS_BYTES).toHex());
obj[QStringLiteral("slot")] = static_cast<qint64>(value->slot);
obj[QStringLiteral("height")] = static_cast<qint64>(value->height);
obj[QStringLiteral("mode")] =
(value->mode == State::Online) ? QStringLiteral("Online") : QStringLiteral("Bootstrapping");
json obj;
obj["lib"] = bytes_to_hex(reinterpret_cast<const uint8_t*>(value->lib), ADDRESS_BYTES);
obj["tip"] = bytes_to_hex(reinterpret_cast<const uint8_t*>(value->tip), ADDRESS_BYTES);
obj["slot"] = static_cast<int64_t>(value->slot);
obj["height"] = static_cast<int64_t>(value->height);
obj["mode"] = (value->mode == State::Online) ? "Online" : "Bootstrapping";
const OperationStatus free_status = free_cryptarchia_info(value);
if (!is_ok(&free_status)) {
qWarning() << "Failed to free cryptarchia info. Error:" << free_status;
fprintf(stderr, "Failed to free cryptarchia info. Error: %d\n", free_status);
}
return QJsonDocument(obj).toJson(QJsonDocument::Compact);
return obj.dump();
}
void LogosBlockchainModule::emit_event(const QString& event_name, const QVariantList& data) {
if (!logosAPI) {
qWarning() << "LogosBlockchainModule: LogosAPI not available, cannot emit" << event_name;
return;
}
if (!client) {
qWarning() << "LogosBlockchainModule: Failed to get liblogos_blockchain_module client for event" << event_name;
return;
}
client->onEventResponse(this, event_name, data);
}

View File

@ -1,8 +1,10 @@
#pragma once
#include "i_logos_blockchain_module.h"
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
#include <iostream>
#ifdef __cplusplus
extern "C" {
#endif
@ -11,75 +13,56 @@ extern "C" {
}
#endif
class LogosBlockchainModule final : public QObject, public PluginInterface, public ILogosBlockchainModule {
Q_OBJECT
Q_PLUGIN_METADATA(IID ILogosBlockchainModule_iid FILE LOGOS_BLOCKCHAIN_MODULE_METADATA_FILE)
Q_INTERFACES(PluginInterface)
class LogosBlockchainModule {
public:
LogosBlockchainModule();
~LogosBlockchainModule() override;
~LogosBlockchainModule();
// Logos Core
[[nodiscard]] QString name() const override;
[[nodiscard]] QString version() const override;
Q_INVOKABLE void initLogos(LogosAPI*) override;
// Wired automatically by the generated glue layer.
// Call this to emit named events to other modules / the host application.
// Data is a JSON-encoded string (object or array).
std::function<void(const std::string& eventName, const std::string& data)> emitEvent;
// ---- Node ----
// Lifecycle
Q_INVOKABLE int generate_user_config(const QVariantMap& args) override;
Q_INVOKABLE int generate_user_config_from_str(const QString& args) override;
Q_INVOKABLE int start(const QString& config_path, const QString& deployment) override;
Q_INVOKABLE int stop() override;
int generate_user_config(const std::string& json_args);
int start(const std::string& config_path, const std::string& deployment);
int stop();
// Wallet
Q_INVOKABLE QString wallet_get_balance(const QString& address_hex) override;
Q_INVOKABLE QString wallet_transfer_funds(
const QString& change_public_key,
const QStringList& sender_addresses,
const QString& recipient_address,
const QString& amount,
const QString& optional_tip_hex
) override;
Q_INVOKABLE QString wallet_transfer_funds(
const QString& change_public_key,
const QString& sender_address,
const QString& recipient_address,
const QString& amount,
const QString& optional_tip_hex
std::string wallet_get_balance(const std::string& address_hex);
std::string wallet_transfer_funds(
const std::string& change_public_key,
const std::vector<std::string>& sender_addresses,
const std::string& recipient_address,
const std::string& amount,
const std::string& optional_tip_hex
);
Q_INVOKABLE QStringList wallet_get_known_addresses() override;
std::vector<std::string> wallet_get_known_addresses();
// Blend
Q_INVOKABLE QString blend_join_as_core_node(
const QString& provider_id_hex,
const QString& zk_id_hex,
const QString& locked_note_id_hex,
const QStringList& locators
) override;
std::string blend_join_as_core_node(
const std::string& provider_id_hex,
const std::string& zk_id_hex,
const std::string& locked_note_id_hex,
const std::vector<std::string>& locators
);
// Explorer
Q_INVOKABLE QString get_block(const QString& header_id_hex) override;
Q_INVOKABLE QString get_blocks(quint64 from_slot, quint64 to_slot) override;
Q_INVOKABLE QString get_transaction(const QString& tx_hash_hex) override;
std::string get_block(const std::string& header_id_hex);
std::string get_blocks(uint64_t from_slot, uint64_t to_slot);
std::string get_transaction(const std::string& tx_hash_hex);
// Cryptarchia
Q_INVOKABLE QString get_cryptarchia_info() override;
signals:
void eventResponse(const QString& event_name, const QVariantList& data);
std::string get_cryptarchia_info();
private:
LogosBlockchainNode* node = nullptr;
LogosAPIClient* client = nullptr;
// Static instance for C callback (C API doesn't support user data)
static LogosBlockchainModule* s_instance;
// C-compatible callback function
static void on_new_block_callback(const char* block);
// Helper method for emitting events
void emit_event(const QString& event_name, const QVariantList& data);
};

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

729
tests/test_blockchain.cpp Normal file
View File

@ -0,0 +1,729 @@
// 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 <cstdlib>
#include <filesystem>
#include <fstream>
#include <string>
#include <unistd.h>
#include <vector>
namespace fs = std::filesystem;
// 64-char hex string = 32 bytes (valid address/hash)
static const std::string VALID_HEX(64, 'a');
static const std::string VALID_HEX_WITH_PREFIX = "0x" + std::string(64, 'b');
static bool starts_with(const std::string& s, const std::string& prefix) {
return s.size() >= prefix.size() && s.compare(0, prefix.size(), prefix) == 0;
}
static bool contains(const std::string& s, const std::string& sub) {
return s.find(sub) != std::string::npos;
}
// RAII wrapper for a temporary directory (removed on destruction).
struct TempDir {
fs::path path;
TempDir() {
char tmpl[] = "/tmp/logos-blockchain-test-XXXXXX";
char* dir = mkdtemp(tmpl);
if (dir) path = dir;
}
~TempDir() {
if (!path.empty()) {
std::error_code ec;
fs::remove_all(path, ec);
}
}
bool isValid() const { return !path.empty(); }
std::string filePath(const std::string& name) const { return (path / name).string(); }
};
// Helper: create a module with a running (mocked) node.
// Sets up circuits directory, LOGOS_MODULE_PATH env, and calls start().
static LogosBlockchainModule* createStartedModule(LogosTestContext& t, TempDir& tmpDir) {
fs::create_directories(tmpDir.path / "circuits");
{
std::ofstream f((tmpDir.path / "circuits" / "dummy.bin").string());
f << "x";
}
setenv("LOGOS_MODULE_PATH", tmpDir.path.string().c_str(), 1);
auto* module = new LogosBlockchainModule();
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;
}
// ============================================================================
// 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);
LOGOS_ASSERT_EQ(module.generate_user_config(R"({"output":"/tmp/test-config.json"})"), 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);
LOGOS_ASSERT_EQ(module.generate_user_config("{}"), 1);
}
LOGOS_TEST(generate_user_config_from_json_string) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
t.mockCFunction("generate_user_config").returns(0);
LOGOS_ASSERT_EQ(module.generate_user_config(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);
std::string args = R"({
"initial_peers": ["peer1", "peer2"],
"output": "/tmp/out.json",
"net_port": 9000,
"blend_port": 9001,
"http_addr": "0.0.0.0:8080",
"external_address": "1.2.3.4",
"no_public_ip_check": true,
"deployment": { "well_known_deployment": "devnet" },
"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;
std::string result = module.wallet_get_balance(VALID_HEX);
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "not running"));
}
LOGOS_TEST(wallet_transfer_funds_without_node_returns_error) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
std::string result = module.wallet_transfer_funds(VALID_HEX, {VALID_HEX}, VALID_HEX, "100", "");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "not running"));
}
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().empty());
}
LOGOS_TEST(blend_join_as_core_node_without_node_returns_error) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
std::string result = module.blend_join_as_core_node(VALID_HEX, VALID_HEX, VALID_HEX, {"locator1"});
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "not running"));
}
LOGOS_TEST(get_block_without_node_returns_error) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
LOGOS_ASSERT_TRUE(starts_with(module.get_block(VALID_HEX), "Error:"));
}
LOGOS_TEST(get_blocks_without_node_returns_error) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
LOGOS_ASSERT_TRUE(starts_with(module.get_blocks(0, 10), "Error:"));
}
LOGOS_TEST(get_transaction_without_node_returns_error) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
LOGOS_ASSERT_TRUE(starts_with(module.get_transaction(VALID_HEX), "Error:"));
}
LOGOS_TEST(get_cryptarchia_info_without_node_returns_error) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
LOGOS_ASSERT_TRUE(starts_with(module.get_cryptarchia_info(), "Error:"));
}
// ============================================================================
// Node lifecycle (start / stop)
// ============================================================================
LOGOS_TEST(start_succeeds_with_mocked_dependencies) {
auto t = LogosTestContext("blockchain_module");
TempDir 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");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
LOGOS_ASSERT_EQ(module->start("/tmp/config.json", ""), 1);
delete module;
}
LOGOS_TEST(stop_succeeds_with_running_node) {
auto t = LogosTestContext("blockchain_module");
TempDir 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");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
std::string result = module->wallet_get_balance("abcd");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "64 hex"));
delete module;
}
LOGOS_TEST(wallet_get_balance_rejects_long_hex) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
std::string result = module->wallet_get_balance(std::string(66, 'a'));
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
delete module;
}
LOGOS_TEST(wallet_get_balance_rejects_invalid_chars) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
std::string hex = std::string(62, 'a') + "zz";
std::string result = module->wallet_get_balance(hex);
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
delete module;
}
// wallet_transfer_funds validation
LOGOS_TEST(wallet_transfer_funds_rejects_invalid_amount) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
std::string result = module->wallet_transfer_funds(VALID_HEX, {VALID_HEX}, VALID_HEX, "not_a_number", "");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "Invalid amount"));
delete module;
}
LOGOS_TEST(wallet_transfer_funds_rejects_invalid_change_key) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
std::string result = module->wallet_transfer_funds("bad", {VALID_HEX}, VALID_HEX, "100", "");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "change_public_key"));
delete module;
}
LOGOS_TEST(wallet_transfer_funds_rejects_invalid_recipient) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
std::string result = module->wallet_transfer_funds(VALID_HEX, {VALID_HEX}, "short", "100", "");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "recipient_address"));
delete module;
}
LOGOS_TEST(wallet_transfer_funds_rejects_empty_senders) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
std::string result = module->wallet_transfer_funds(VALID_HEX, {}, VALID_HEX, "100", "");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "sender"));
delete module;
}
LOGOS_TEST(wallet_transfer_funds_rejects_invalid_sender_address) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
std::string result = module->wallet_transfer_funds(VALID_HEX, {"bad_addr"}, VALID_HEX, "100", "");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "sender"));
delete module;
}
LOGOS_TEST(wallet_transfer_funds_rejects_invalid_optional_tip) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
std::string result = module->wallet_transfer_funds(VALID_HEX, {VALID_HEX}, VALID_HEX, "100", "bad_tip");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "tip"));
delete module;
}
// blend_join_as_core_node validation
LOGOS_TEST(blend_join_rejects_invalid_provider_id) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
std::string result = module->blend_join_as_core_node("short", VALID_HEX, VALID_HEX, {});
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "provider_id"));
delete module;
}
LOGOS_TEST(blend_join_rejects_invalid_zk_id) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
std::string result = module->blend_join_as_core_node(VALID_HEX, "short", VALID_HEX, {});
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "zk_id"));
delete module;
}
LOGOS_TEST(blend_join_rejects_invalid_locked_note_id) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
std::string result = module->blend_join_as_core_node(VALID_HEX, VALID_HEX, "short", {});
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "locked_note_id"));
delete module;
}
// get_block / get_transaction validation
LOGOS_TEST(get_block_rejects_invalid_hex) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
std::string result = module->get_block("tooshort");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "64 hex"));
delete module;
}
LOGOS_TEST(get_transaction_rejects_invalid_hex) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
std::string result = module->get_transaction("bad");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "64 hex"));
delete module;
}
// ============================================================================
// 0x prefix handling
// ============================================================================
LOGOS_TEST(wallet_get_balance_accepts_0x_prefix) {
auto t = LogosTestContext("blockchain_module");
TempDir 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);
std::string result = module->wallet_get_balance(VALID_HEX_WITH_PREFIX);
LOGOS_ASSERT_EQ(result, std::string("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");
TempDir 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);
std::string result = module->wallet_get_balance(VALID_HEX);
LOGOS_ASSERT_EQ(result, std::string("1000"));
LOGOS_ASSERT(t.cFunctionCalled("get_balance"));
delete module;
}
LOGOS_TEST(wallet_get_balance_returns_error_on_ffi_failure) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("get_balance_error").returns(1);
std::string result = module->wallet_get_balance(VALID_HEX);
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
delete module;
}
LOGOS_TEST(wallet_transfer_funds_returns_tx_hash) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("transfer_funds_error").returns(0);
std::string result = module->wallet_transfer_funds(VALID_HEX, {VALID_HEX}, VALID_HEX, "500", "");
LOGOS_ASSERT_FALSE(starts_with(result, "Error:"));
LOGOS_ASSERT_EQ(static_cast<int>(result.length()), 64);
LOGOS_ASSERT_TRUE(starts_with(result, "ab"));
LOGOS_ASSERT(t.cFunctionCalled("transfer_funds"));
delete module;
}
LOGOS_TEST(wallet_transfer_funds_with_optional_tip) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("transfer_funds_error").returns(0);
std::string result = module->wallet_transfer_funds(VALID_HEX, {VALID_HEX}, VALID_HEX, "100", VALID_HEX);
LOGOS_ASSERT_FALSE(starts_with(result, "Error:"));
LOGOS_ASSERT_EQ(static_cast<int>(result.length()), 64);
delete module;
}
LOGOS_TEST(wallet_transfer_funds_returns_error_on_ffi_failure) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("transfer_funds_error").returns(1);
std::string result = module->wallet_transfer_funds(VALID_HEX, {VALID_HEX}, VALID_HEX, "100", "");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
delete module;
}
LOGOS_TEST(wallet_transfer_funds_single_sender_via_vector) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("transfer_funds_error").returns(0);
std::string result = module->wallet_transfer_funds(VALID_HEX, {VALID_HEX}, VALID_HEX, "100", "");
LOGOS_ASSERT_FALSE(starts_with(result, "Error:"));
LOGOS_ASSERT(t.cFunctionCalled("transfer_funds"));
delete module;
}
LOGOS_TEST(wallet_transfer_funds_multiple_senders) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("transfer_funds_error").returns(0);
std::vector<std::string> senders = {VALID_HEX, std::string(64, 'b')};
std::string result = module->wallet_transfer_funds(VALID_HEX, senders, VALID_HEX, "200", "");
LOGOS_ASSERT_FALSE(starts_with(result, "Error:"));
delete module;
}
LOGOS_TEST(wallet_get_known_addresses_returns_addresses) {
auto t = LogosTestContext("blockchain_module");
TempDir 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);
std::vector<std::string> addrs = module->wallet_get_known_addresses();
LOGOS_ASSERT_EQ(static_cast<int>(addrs.size()), 2);
// Mock fills addr0 with 0x11 -> hex "1111...11", addr1 with 0x22 -> "2222...22"
LOGOS_ASSERT_EQ(addrs[0], std::string(64, '1'));
LOGOS_ASSERT_EQ(addrs[1], std::string(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");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("get_known_addresses_error").returns(1);
std::vector<std::string> addrs = module->wallet_get_known_addresses();
LOGOS_ASSERT_TRUE(addrs.empty());
delete module;
}
// Blend
LOGOS_TEST(blend_join_as_core_node_returns_declaration_id) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("blend_join_as_core_node_error").returns(0);
std::vector<std::string> locators = {"locator1", "locator2"};
std::string 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(static_cast<int>(result.length()), 64);
LOGOS_ASSERT_TRUE(starts_with(result, "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");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("blend_join_as_core_node_error").returns(1);
std::string result = module->blend_join_as_core_node(VALID_HEX, VALID_HEX, VALID_HEX, {});
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
delete module;
}
// Explorer
LOGOS_TEST(get_block_returns_json_on_success) {
auto t = LogosTestContext("blockchain_module");
TempDir 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);
std::string result = module->get_block(VALID_HEX);
LOGOS_ASSERT_TRUE(contains(result, "slot"));
LOGOS_ASSERT_TRUE(contains(result, "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");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("get_block_error").returns(1);
std::string result = module->get_block(VALID_HEX);
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
delete module;
}
LOGOS_TEST(get_blocks_returns_json_on_success) {
auto t = LogosTestContext("blockchain_module");
TempDir 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);
std::string result = module->get_blocks(1, 10);
LOGOS_ASSERT_TRUE(contains(result, "slot"));
LOGOS_ASSERT(t.cFunctionCalled("get_blocks"));
delete module;
}
LOGOS_TEST(get_blocks_returns_error_on_ffi_failure) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("get_blocks_error").returns(1);
LOGOS_ASSERT_TRUE(starts_with(module->get_blocks(0, 10), "Error:"));
delete module;
}
LOGOS_TEST(get_transaction_returns_json_on_success) {
auto t = LogosTestContext("blockchain_module");
TempDir 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);
std::string result = module->get_transaction(VALID_HEX);
LOGOS_ASSERT_TRUE(contains(result, "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");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("get_transaction_error").returns(1);
LOGOS_ASSERT_TRUE(starts_with(module->get_transaction(VALID_HEX), "Error:"));
delete module;
}
// Cryptarchia
LOGOS_TEST(get_cryptarchia_info_returns_json_on_success) {
auto t = LogosTestContext("blockchain_module");
TempDir 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
std::string result = module->get_cryptarchia_info();
LOGOS_ASSERT_FALSE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "slot"));
LOGOS_ASSERT_TRUE(contains(result, "100"));
LOGOS_ASSERT_TRUE(contains(result, "height"));
LOGOS_ASSERT_TRUE(contains(result, "50"));
LOGOS_ASSERT_TRUE(contains(result, "Online"));
LOGOS_ASSERT_TRUE(contains(result, "lib"));
LOGOS_ASSERT_TRUE(contains(result, "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");
TempDir 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
std::string result = module->get_cryptarchia_info();
LOGOS_ASSERT_TRUE(contains(result, "Bootstrapping"));
delete module;
}
LOGOS_TEST(get_cryptarchia_info_returns_error_on_ffi_failure) {
auto t = LogosTestContext("blockchain_module");
TempDir tmpDir;
auto* module = createStartedModule(t, tmpDir);
LOGOS_ASSERT_TRUE(module != nullptr);
t.mockCFunction("get_cryptarchia_info_error").returns(1);
LOGOS_ASSERT_TRUE(starts_with(module->get_cryptarchia_info(), "Error:"));
delete module;
}