Merge 07cf57ef06ab586f762ec1cf311ce5ad4d258080 into 9c8e434900b6cf5c9a3dd837276fa3b3bf461f60

This commit is contained in:
Sasha 2026-05-16 00:20:13 +02:00 committed by GitHub
commit 9ed273012c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 1959 additions and 2187 deletions

View File

@ -1,219 +1,27 @@
cmake_minimum_required(VERSION 3.16)
project(BlockchainUIPlugin VERSION 1.0.0 LANGUAGES CXX)
cmake_minimum_required(VERSION 3.14)
project(BlockchainUiPlugin LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
# Allow override from environment or command line
if(NOT DEFINED LOGOS_LIBLOGOS_ROOT)
set(_parent_liblogos "${CMAKE_SOURCE_DIR}/../logos-liblogos")
set(_use_vendor ${LOGOS_BLOCKCHAIN_UI_USE_VENDOR})
if(NOT _use_vendor)
if(NOT EXISTS "${_parent_liblogos}/interface.h")
set(_use_vendor ON)
endif()
endif()
if(_use_vendor)
set(LOGOS_LIBLOGOS_ROOT "${CMAKE_SOURCE_DIR}/vendor/logos-liblogos")
else()
set(LOGOS_LIBLOGOS_ROOT "${_parent_liblogos}")
endif()
endif()
if(NOT DEFINED LOGOS_CPP_SDK_ROOT)
set(_parent_cpp_sdk "${CMAKE_SOURCE_DIR}/../logos-cpp-sdk")
set(_use_vendor ${LOGOS_BLOCKCHAIN_UI_USE_VENDOR})
if(NOT _use_vendor)
if(NOT EXISTS "${_parent_cpp_sdk}/cpp/logos_api.h")
set(_use_vendor ON)
endif()
endif()
if(_use_vendor)
set(LOGOS_CPP_SDK_ROOT "${CMAKE_SOURCE_DIR}/vendor/logos-cpp-sdk")
else()
set(LOGOS_CPP_SDK_ROOT "${_parent_cpp_sdk}")
endif()
endif()
# Check if dependencies are available (support both source and installed layouts)
set(_liblogos_found FALSE)
if(EXISTS "${LOGOS_LIBLOGOS_ROOT}/interface.h")
set(_liblogos_found TRUE)
set(_liblogos_is_source TRUE)
elseif(EXISTS "${LOGOS_LIBLOGOS_ROOT}/include/interface.h")
set(_liblogos_found TRUE)
set(_liblogos_is_source FALSE)
endif()
set(_cpp_sdk_found FALSE)
if(EXISTS "${LOGOS_CPP_SDK_ROOT}/cpp/logos_api.h")
set(_cpp_sdk_found TRUE)
set(_cpp_sdk_is_source TRUE)
elseif(EXISTS "${LOGOS_CPP_SDK_ROOT}/include/cpp/logos_api.h")
set(_cpp_sdk_found TRUE)
set(_cpp_sdk_is_source FALSE)
endif()
if(NOT _liblogos_found)
message(FATAL_ERROR "logos-liblogos not found at ${LOGOS_LIBLOGOS_ROOT}. "
"Set LOGOS_LIBLOGOS_ROOT or run git submodule update --init --recursive.")
endif()
if(NOT _cpp_sdk_found)
message(FATAL_ERROR "logos-cpp-sdk not found at ${LOGOS_CPP_SDK_ROOT}. "
"Set LOGOS_CPP_SDK_ROOT or run git submodule update --init --recursive.")
endif()
# Find Qt packages
find_package(Qt6 REQUIRED COMPONENTS Core Widgets RemoteObjects Quick QuickWidgets)
# Try to find the component-interfaces package first
find_package(component-interfaces QUIET)
# If not found, use the local interfaces folder
if(NOT component-interfaces_FOUND)
# Include the local interfaces directory
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/interfaces)
# Create a component-interfaces library
add_library(component-interfaces INTERFACE)
target_include_directories(component-interfaces INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/interfaces)
endif()
# Source files
set(SOURCES
src/AccountsModel.cpp
src/AccountsModel.h
src/BlockchainPlugin.cpp
src/BlockchainPlugin.h
src/BlockchainBackend.cpp
src/BlockchainBackend.h
src/LogModel.cpp
src/LogModel.h
src/blockchain_resources.qrc
)
# Add SDK sources (only if source layout, installed layout uses the library)
if(_cpp_sdk_is_source)
list(APPEND SOURCES
${LOGOS_CPP_SDK_ROOT}/cpp/logos_api.cpp
${LOGOS_CPP_SDK_ROOT}/cpp/logos_api.h
${LOGOS_CPP_SDK_ROOT}/cpp/logos_api_client.cpp
${LOGOS_CPP_SDK_ROOT}/cpp/logos_api_client.h
${LOGOS_CPP_SDK_ROOT}/cpp/logos_api_consumer.cpp
${LOGOS_CPP_SDK_ROOT}/cpp/logos_api_consumer.h
${LOGOS_CPP_SDK_ROOT}/cpp/logos_api_provider.cpp
${LOGOS_CPP_SDK_ROOT}/cpp/logos_api_provider.h
${LOGOS_CPP_SDK_ROOT}/cpp/token_manager.cpp
${LOGOS_CPP_SDK_ROOT}/cpp/token_manager.h
${LOGOS_CPP_SDK_ROOT}/cpp/module_proxy.cpp
${LOGOS_CPP_SDK_ROOT}/cpp/module_proxy.h
)
endif()
# Create the plugin library
add_library(blockchain_ui SHARED ${SOURCES})
# Set output name without lib prefix
set_target_properties(blockchain_ui PROPERTIES
PREFIX ""
OUTPUT_NAME "blockchain_ui"
)
# Include directories
target_include_directories(blockchain_ui PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_BINARY_DIR}
)
# Add include directories based on layout type
if(_liblogos_is_source)
target_include_directories(blockchain_ui PRIVATE ${LOGOS_LIBLOGOS_ROOT})
if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT})
include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake)
else()
target_include_directories(blockchain_ui PRIVATE ${LOGOS_LIBLOGOS_ROOT}/include)
message(FATAL_ERROR "LogosModule.cmake not found. Set LOGOS_MODULE_BUILDER_ROOT.")
endif()
if(_cpp_sdk_is_source)
target_include_directories(blockchain_ui PRIVATE
${LOGOS_CPP_SDK_ROOT}/cpp
)
else()
target_include_directories(blockchain_ui PRIVATE
${LOGOS_CPP_SDK_ROOT}/include
${LOGOS_CPP_SDK_ROOT}/include/cpp
${LOGOS_CPP_SDK_ROOT}/include/core
)
endif()
# Link against libraries
target_link_libraries(blockchain_ui PRIVATE
Qt6::Core
Qt6::Widgets
Qt6::RemoteObjects
Qt6::Quick
Qt6::QuickWidgets
component-interfaces
logos_module(
NAME blockchain_ui
REP_FILE src/BlockchainBackend.rep
SOURCES
src/BlockchainPluginInterface.h
src/BlockchainPlugin.h
src/BlockchainPlugin.cpp
src/BlockchainBackend.h
src/BlockchainBackend.cpp
src/AccountsModel.h
src/AccountsModel.cpp
src/LogModel.h
src/LogModel.cpp
FIND_PACKAGES
Qt6Gui
LINK_LIBRARIES
Qt6::Gui
)
# When using installed SDK layout (e.g. Nix), link the pre-built SDK library
if(NOT _cpp_sdk_is_source)
find_library(LOGOS_SDK_LIB logos_sdk PATHS ${LOGOS_CPP_SDK_ROOT}/lib NO_DEFAULT_PATH)
if(LOGOS_SDK_LIB)
target_link_libraries(blockchain_ui PRIVATE ${LOGOS_SDK_LIB})
else()
message(FATAL_ERROR "logos_sdk library not found in ${LOGOS_CPP_SDK_ROOT}/lib - required when using installed SDK layout")
endif()
endif()
# Link against Abseil libraries if found
find_package(absl QUIET)
if(absl_FOUND)
target_link_libraries(blockchain_ui PRIVATE
absl::base
absl::strings
absl::log
absl::check
)
endif()
# Set common properties for both platforms
set_target_properties(blockchain_ui PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/modules"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/modules" # For Windows .dll
BUILD_WITH_INSTALL_RPATH TRUE
SKIP_BUILD_RPATH FALSE)
if(APPLE)
# macOS specific settings
set_target_properties(blockchain_ui PROPERTIES
INSTALL_RPATH "@loader_path"
INSTALL_NAME_DIR "@rpath"
BUILD_WITH_INSTALL_NAME_DIR TRUE)
add_custom_command(TARGET blockchain_ui POST_BUILD
COMMAND install_name_tool -id "@rpath/blockchain_ui.dylib" $<TARGET_FILE:blockchain_ui>
COMMENT "Updating library paths for macOS"
)
else()
# Linux specific settings
set_target_properties(blockchain_ui PROPERTIES
INSTALL_RPATH "$ORIGIN"
INSTALL_RPATH_USE_LINK_PATH FALSE)
endif()
install(TARGETS blockchain_ui
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/logos/modules
RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR}/logos/modules
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}/logos/modules
)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/metadata.json
DESTINATION ${CMAKE_INSTALL_DATADIR}/logos-blockchain-ui
)
# Print status messages
message(STATUS "Blockchain UI Plugin configured successfully")

154
README.md
View File

@ -1,6 +1,8 @@
# logos-blockchain-ui
A Qt UI plugin for the Logos Blockchain Module, providing a graphical interface to control and monitor the Logos blockchain node.
A QML + C++ backend UI module for the [Logos](https://logos.co) platform that provides a graphical interface to control and monitor the Logos blockchain node.
Built with [`logos-module-builder`](https://github.com/logos-co/logos-module-builder) using the `mkLogosQmlModule` pattern (QML frontend + C++ backend with Qt Remote Objects).
## Features
@ -8,139 +10,119 @@ A Qt UI plugin for the Logos Blockchain Module, providing a graphical interface
- Configure node parameters (config path, deployment)
- Check wallet balances
- Monitor node status and information
- Account management
## Standalone App Quickstart
1. Build and run the app with
1. Build and run the app:
```bash
nix run '.#app'
nix run
```
2. Generate a new config using the some initial peers that are part of the live testnet. You can find some peers [here](https://www.notion.so/nomos-tech/Logos-Blockchain-Devnet-Lisbon-March-2026-2fe261aa09df8025ad94e380933b4cf9?source=copy_link#319261aa09df80a6ac9bcb7487d14d6a).
2. Generate a new config using initial peers from the live testnet. Find peers [here](https://www.notion.so/nomos-tech/Logos-Blockchain-Devnet-Lisbon-March-2026-2fe261aa09df8025ad94e380933b4cf9?source=copy_link#319261aa09df80a6ac9bcb7487d14d6a).
3. Start the node, and let it sync with the initial peers. You can track progress by opening a terminal and running:
3. Start the node and let it sync. Track progress:
```bash
watch -n1 'curl -s localhost:8080/cryptarchia/info'
```
And comparing the `height` with the block height in the [block explorer](https://devnet.blockchain.logos.co/web/explorer/).
Compare the `height` with the [block explorer](https://devnet.blockchain.logos.co/web/explorer/).
4. In the meantime, you can request funds from the faucet, copy one of the keys visible in the ui and paste it into the [faucet](https://devnet.blockchain.logos.co/web/faucet/).
4. Request funds from the [faucet](https://devnet.blockchain.logos.co/web/faucet/) — copy one of the keys from the UI and paste it there.
5. Once your node finishes syncing, you can refresh your balance and you should see your funds from the faucet.
5. Once synced, refresh your balance to see your funds.
At this point, you are done. If you leave this node running, your tokens will age sufficiently such that they will become eligible for participation in consensus. This happens automatically, so long as you don't transfer your tokens for ~3.5 hours.
Leaving the node running for ~3.5 hours allows your tokens to age and become eligible for consensus participation (automatic).
For a video walkthrough of this process, see this [recording](https://drive.google.com/file/d/1hw6rQZnuka3Y_JBpUz0WyLXglTSPiZEc/view?usp=drive_link).
For a video walkthrough, see this [recording](https://drive.google.com/file/d/1hw6rQZnuka3Y_JBpUz0WyLXglTSPiZEc/view?usp=drive_link).
## How to Build
## How to Run
### Using Nix (Recommended)
#### Build Complete UI Plugin
### Standalone (recommended for development)
```bash
# Build everything (default)
nix build
# Run directly
nix run
# Or explicitly
nix build '.#default'
# With local workspace overrides
nix run --override-input liblogos_blockchain_module path:../logos-blockchain-module \
--override-input liblogos_blockchain_module/logos-module-builder path:../logos-module-builder
```
The result will include:
- `/lib/blockchain_ui.dylib` (or `.so` on Linux) - The Blockchain UI plugin
#### Build Individual Components
### In Basecamp
```bash
# Build only the library (plugin)
nix build '.#lib'
# Build LGX
nix build .#lgx
# Build the standalone Qt application
nix build '.#app'
# Install into Basecamp's plugin directory
lgpm --ui-plugins-dir ~/Library/Application\ Support/Logos/LogosBasecampDev/plugins \
install --file result/*.lgx
```
#### Development Shell
Or from the workspace:
```bash
# Enter development shell with all dependencies
nix develop
ws bundle logos-blockchain-ui --auto-local
```
**Note:** In zsh, you need to quote the target (e.g., `'.#default'`) to prevent glob expansion.
If you don't have flakes enabled globally, add experimental flags:
### Build Targets
```bash
nix build --extra-experimental-features 'nix-command flakes'
nix build # default — combined plugin + QML output
nix build .#lgx # .lgx package for distribution
nix build .#install # lgpm-installed output (modules/ + plugins/)
nix run # standalone app with blockchain module
nix develop # enter development shell
```
The compiled artifacts can be found at `result/`
## Module Structure
#### Running the Standalone App
After building the app with `nix build '.#app'`, you can run it:
```bash
# Run the standalone Qt application
./result/bin/logos-blockchain-ui-app
```
The app will automatically load the required modules (capability_module, liblogos_blockchain_module) and the blockchain_ui Qt plugin. All dependencies are bundled in the Nix store layout.
#### Nix Organization
The nix build system is organized into modular files in the `/nix` directory:
- `nix/default.nix` - Common configuration (dependencies, flags, metadata)
- `nix/lib.nix` - UI plugin compilation
- `nix/app.nix` - Standalone Qt application compilation
## Output Structure
When built with Nix:
**Library build (`nix build '.#lib'`):**
```
result/
└── lib/
└── blockchain_ui.dylib # Logos Blockchain UI plugin
```
**App build (`nix build '.#app'`):**
```
result/
├── bin/
│ ├── logos-blockchain-ui-app # Standalone Qt application
│ ├── logos_host # Logos host executable (for plugins)
│ └── logoscore # Logos core executable
├── lib/
│ ├── liblogos_core.dylib # Logos core library
│ ├── liblogos_sdk.dylib # Logos SDK library
│ └── Logos/DesignSystem/ # QML design system
├── modules/
│ ├── capability_module_plugin.dylib
│ ├── liblogos_blockchain_module.dylib
│ └── liblogos_blockchain.dylib
└── blockchain_ui.dylib # Qt plugin (loaded by app)
logos-blockchain-ui/
├── flake.nix # mkLogosQmlModule
├── metadata.json # Module config (ui_qml type)
├── CMakeLists.txt # logos_module() macro
└── src/
├── BlockchainBackend.rep # RemoteObject interface
├── BlockchainBackend.h/cpp # Business logic (extends BlockchainBackendSimpleSource)
├── BlockchainPlugin.h/cpp # Thin plugin entry point
├── BlockchainPluginInterface.h # Plugin interface marker
├── AccountsModel.h/cpp # QAbstractListModel for accounts
├── LogModel.h/cpp # QAbstractListModel for logs
└── qml/
└── BlockchainView.qml # QML frontend (+ sub-views)
```
## Configuration
### Blockchain Node Configuration
The blockchain node can be configured in two ways:
1. **Via UI**: Enter the config path in the "Config Path" field
2. **Via Environment Variable**: Set `LB_CONFIG_PATH` to your configuration file path
Example configuration file can be found in the logos-blockchain-module repository at `config/node_config.yaml`.
- **Via UI**: Enter the config path in the "Config Path" field
- **Via Environment Variable**: Set `LB_CONFIG_PATH` to your configuration file path
### QML Hot Reload
During development, you can enable QML hot reload by setting an environment variable:
During development, set the environment variable to load QML from disk:
```bash
export BLOCKCHAIN_UI_QML_PATH=/path/to/logos-blockchain-ui/src/qml
```
This allows you to edit the QML file and see changes by reloading the plugin without recompiling.
## Dependencies
| Dependency | Purpose |
|---|---|
| Qt6 Core, Gui, RemoteObjects, Declarative | UI framework + IPC |
| [`logos-module-builder`](https://github.com/logos-co/logos-module-builder) | Build system (mkLogosQmlModule) |
| [`logos-blockchain-module`](https://github.com/logos-blockchain/logos-blockchain-module) | Blockchain backend module |
## Related Repositories
| Repository | Role |
|---|---|
| [`logos-blockchain-module`](https://github.com/logos-blockchain/logos-blockchain-module) | Blockchain backend — this UI's required dependency |
| [`logos-module-builder`](https://github.com/logos-co/logos-module-builder) | Module build system |
| [`logos-liblogos`](https://github.com/logos-co/logos-liblogos) | Logos Core platform |

View File

@ -1,62 +0,0 @@
cmake_minimum_required(VERSION 3.16)
project(LogosBlockchainUIApp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
# Find Qt packages
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Widgets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Widgets)
# Find logos-liblogos
if(NOT DEFINED LOGOS_LIBLOGOS_ROOT)
message(FATAL_ERROR "LOGOS_LIBLOGOS_ROOT must be defined")
endif()
message(STATUS "Using logos-liblogos at: ${LOGOS_LIBLOGOS_ROOT}")
# Include and link directories (app only uses logos_core from liblogos, not the SDK)
include_directories(
${LOGOS_LIBLOGOS_ROOT}/include
)
link_directories(
${LOGOS_LIBLOGOS_ROOT}/lib
)
# Set output directories
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
# Create the executable
add_executable(logos-blockchain-ui-app
main.cpp
mainwindow.cpp
mainwindow.h
)
# Link libraries
target_link_libraries(logos-blockchain-ui-app PRIVATE
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Widgets
logos_core
)
# Set RPATH settings for the executable
if(APPLE)
set_target_properties(logos-blockchain-ui-app PROPERTIES
INSTALL_RPATH "@executable_path/../lib"
BUILD_WITH_INSTALL_RPATH TRUE
)
elseif(UNIX)
set_target_properties(logos-blockchain-ui-app PROPERTIES
INSTALL_RPATH "$ORIGIN/../lib"
BUILD_WITH_INSTALL_RPATH TRUE
)
endif()
# Install rules
install(TARGETS logos-blockchain-ui-app
RUNTIME DESTINATION bin
)

View File

@ -1,56 +0,0 @@
#include "mainwindow.h"
#include <QApplication>
#include <QDir>
#include <QDebug>
#include <iostream>
#include <memory>
// CoreManager C API functions
extern "C" {
void logos_core_set_plugins_dir(const char* plugins_dir);
void logos_core_start();
void logos_core_cleanup();
char** logos_core_get_loaded_plugins();
int logos_core_load_plugin(const char* plugin_name);
}
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QString pluginsDir = QDir::cleanPath(QCoreApplication::applicationDirPath() + "/../modules");
logos_core_set_plugins_dir(pluginsDir.toUtf8().constData());
logos_core_start();
if (!logos_core_load_plugin("capability_module")) {
qWarning() << "Failed to load capability_module plugin";
}
if (!logos_core_load_plugin("liblogos_blockchain_module")) {
qWarning() << "Failed to load blockchain module plugin";
}
char** loadedPlugins = logos_core_get_loaded_plugins();
int count = 0;
if (loadedPlugins) {
qInfo() << "Currently loaded plugins:";
for (char** p = loadedPlugins; *p != nullptr; ++p) {
qInfo() << " -" << *p;
++count;
}
qInfo() << "Total plugins:" << count;
} else {
qInfo() << "No plugins loaded.";
}
MainWindow window;
window.show();
int result = app.exec();
logos_core_cleanup();
return result;
}

View File

@ -1,66 +0,0 @@
#include <QtWidgets>
#include "mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
setupUi();
}
MainWindow::~MainWindow()
{
}
void MainWindow::setupUi()
{
// Determine the appropriate plugin extension based on the platform
QString pluginExtension;
#if defined(Q_OS_WIN)
pluginExtension = ".dll";
#elif defined(Q_OS_MAC)
pluginExtension = ".dylib";
#else // Linux and other Unix-like systems
pluginExtension = ".so";
#endif
QString pluginPath = QCoreApplication::applicationDirPath() + "/../blockchain_ui" + pluginExtension;
QPluginLoader loader(pluginPath);
QWidget* blockchainWidget = nullptr;
if (loader.load()) {
QObject* plugin = loader.instance();
if (plugin) {
// Try to create the blockchain widget using the plugin's createWidget method
QMetaObject::invokeMethod(plugin, "createWidget",
Qt::DirectConnection,
Q_RETURN_ARG(QWidget*, blockchainWidget));
}
}
if (blockchainWidget) {
setCentralWidget(blockchainWidget);
} else {
qWarning() << "================================================";
qWarning() << "Failed to load blockchain UI plugin from:" << pluginPath;
qWarning() << "Error:" << loader.errorString();
qWarning() << "================================================";
// Fallback: show a message when plugin is not found
QWidget* fallbackWidget = new QWidget(this);
QVBoxLayout* layout = new QVBoxLayout(fallbackWidget);
QLabel* messageLabel = new QLabel("Blockchain UI module not loaded", fallbackWidget);
QFont font = messageLabel->font();
font.setPointSize(14);
messageLabel->setFont(font);
messageLabel->setAlignment(Qt::AlignCenter);
layout->addWidget(messageLabel);
setCentralWidget(fallbackWidget);
}
// Set window title and size
setWindowTitle("Logos Blockchain UI App");
resize(800, 600);
}

View File

@ -1,18 +0,0 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
void setupUi();
};
#endif // MAINWINDOW_H

2038
flake.lock generated

File diff suppressed because it is too large Load Diff

139
flake.nix
View File

@ -1,82 +1,71 @@
{
description = "Logos Blockchain UI - A Qt UI plugin for Logos Blockchain Module";
description = "Blockchain UI plugin for the Logos application";
inputs = {
# Follow the same nixpkgs as logos-liblogos to ensure compatibility
nixpkgs.follows = "logos-liblogos/nixpkgs";
logos-cpp-sdk.url = "github:logos-co/logos-cpp-sdk";
logos-liblogos.url = "github:logos-co/logos-liblogos?rev=e3741c01fd3abf6b7bd9ff2fa8edf89c41fc0cea";
logos-blockchain-module.url = "github:logos-blockchain/logos-blockchain-module";
logos-capability-module.url = "github:logos-co/logos-capability-module";
logos-design-system.url = "github:logos-co/logos-design-system";
logos-design-system.inputs.nixpkgs.follows = "nixpkgs";
# Promoted to direct inputs so they can be overridden with one --override-input each.
logos-cpp-sdk.url = "github:logos-co/logos-cpp-sdk";
logos-module.url = "github:logos-co/logos-module";
logos-liblogos.url = "github:logos-co/logos-liblogos";
logos-capability-module.url = "github:logos-co/logos-capability-module";
logos-package-manager.url = "github:logos-co/logos-package-manager";
process-stats.url = "github:logos-co/process-stats";
logos-view-module-runtime.url = "github:logos-co/logos-view-module-runtime";
logos-standalone-app.url = "github:logos-co/logos-standalone-app";
logos-plugin-qt.url = "github:logos-co/logos-plugin-qt";
nix-bundle-lgx.url = "github:logos-co/nix-bundle-lgx";
# All nixpkgs in the closure must come from logos-cpp-sdk's logos-nix to keep one Qt.
nixpkgs.follows = "logos-cpp-sdk/logos-nix/nixpkgs";
# ── Force every direct input's nixpkgs to the unified one ──
logos-module.inputs.nixpkgs.follows = "nixpkgs";
logos-liblogos.inputs.nixpkgs.follows = "nixpkgs";
logos-capability-module.inputs.nixpkgs.follows = "nixpkgs";
logos-package-manager.inputs.nixpkgs.follows = "nixpkgs";
process-stats.inputs.nixpkgs.follows = "nixpkgs";
logos-view-module-runtime.inputs.nixpkgs.follows = "nixpkgs";
logos-standalone-app.inputs.nixpkgs.follows = "nixpkgs";
logos-plugin-qt.inputs.nixpkgs.follows = "nixpkgs";
nix-bundle-lgx.inputs.nixpkgs.follows = "nixpkgs";
logos-module-builder.inputs.nixpkgs.follows = "nixpkgs";
# ── logos-module-builder: rewire its logos-* deps to our top-level pins ──
logos-module-builder.url = "github:logos-co/logos-module-builder";
logos-module-builder.inputs.logos-cpp-sdk.follows = "logos-cpp-sdk";
logos-module-builder.inputs.logos-module.follows = "logos-module";
logos-module-builder.inputs.logos-plugin-qt.follows = "logos-plugin-qt";
logos-module-builder.inputs.logos-plugin-core.follows = "logos-plugin-qt";
logos-module-builder.inputs.logos-standalone-app.follows = "logos-standalone-app";
logos-module-builder.inputs.nix-bundle-lgx.follows = "nix-bundle-lgx";
# ── logos-standalone-app: rewire its logos-* deps too ──
logos-standalone-app.inputs.logos-cpp-sdk.follows = "logos-cpp-sdk";
logos-standalone-app.inputs.logos-liblogos.follows = "logos-liblogos";
logos-standalone-app.inputs.logos-capability-module.follows = "logos-capability-module";
logos-standalone-app.inputs.logos-view-module-runtime.follows = "logos-view-module-runtime";
logos-standalone-app.inputs.nix-bundle-lgx.follows = "nix-bundle-lgx";
# ── logos-liblogos: rewire its logos-* deps ──
logos-liblogos.inputs.logos-cpp-sdk.follows = "logos-cpp-sdk";
logos-liblogos.inputs.logos-module.follows = "logos-module";
logos-liblogos.inputs.logos-capability-module.follows = "logos-capability-module";
logos-liblogos.inputs.logos-package-manager.follows = "logos-package-manager";
logos-liblogos.inputs.process-stats.follows = "process-stats";
logos-capability-module.inputs.logos-cpp-sdk.follows = "logos-cpp-sdk";
logos-capability-module.inputs.logos-module.follows = "logos-module";
logos-plugin-qt.inputs.logos-module.follows = "logos-module";
liblogos_blockchain_module.url = "github:logos-blockchain/logos-blockchain-module/09eda0211df54b45d88d912aea28498d427ddada";
liblogos_blockchain_module.inputs.logos-module-builder.follows = "logos-module-builder";
};
outputs = { self, nixpkgs, logos-cpp-sdk, logos-liblogos, logos-blockchain-module, logos-capability-module, logos-design-system }:
let
systems = [ "aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux" ];
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f {
pkgs = import nixpkgs { inherit system; };
logosSdk = logos-cpp-sdk.packages.${system}.default;
logosLiblogos = logos-liblogos.packages.${system}.default;
logosBlockchainModule = logos-blockchain-module.packages.${system}.default;
logosCapabilityModule = logos-capability-module.packages.${system}.default;
logosDesignSystem = logos-design-system.packages.${system}.default;
});
in
{
packages = forAllSystems ({ pkgs, logosSdk, logosLiblogos, logosBlockchainModule, logosCapabilityModule, logosDesignSystem }:
let
# Common configuration
common = import ./nix/default.nix {
inherit pkgs logosSdk logosLiblogos;
};
src = ./.;
# Library package (default blockchain-module has lib + include via symlinkJoin)
lib = import ./nix/lib.nix {
inherit pkgs common src logosBlockchainModule;
};
# App package
app = import ./nix/app.nix {
inherit pkgs common src logosLiblogos logosBlockchainModule logosCapabilityModule logosDesignSystem;
logosBlockchainUI = lib;
};
in
{
# Individual outputs
logos-blockchain-ui-lib = lib;
app = app;
lib = lib;
# Default package
default = lib;
}
);
devShells = forAllSystems ({ pkgs, logosSdk, logosLiblogos, logosBlockchainModule, logosCapabilityModule, logosDesignSystem }: {
default = pkgs.mkShell {
nativeBuildInputs = [
pkgs.cmake
pkgs.ninja
pkgs.pkg-config
];
buildInputs = [
pkgs.qt6.qtbase
pkgs.qt6.qtremoteobjects
pkgs.zstd
pkgs.krb5
pkgs.abseil-cpp
];
shellHook = ''
export LOGOS_LIBLOGOS_ROOT="${logosLiblogos}"
export LOGOS_DESIGN_SYSTEM_ROOT="${logosDesignSystem}"
echo "Logos Blockchain UI development environment"
echo "LOGOS_LIBLOGOS_ROOT: $LOGOS_LIBLOGOS_ROOT"
'';
};
});
outputs = inputs@{ logos-module-builder, ... }:
logos-module-builder.lib.mkLogosQmlModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
};
}

View File

@ -1,17 +0,0 @@
#pragma once
#include <QObject>
#include <QWidget>
#include <QtPlugin>
class LogosAPI;
class IComponent {
public:
virtual ~IComponent() = default;
virtual QWidget* createWidget(LogosAPI* logosAPI = nullptr) = 0;
virtual void destroyWidget(QWidget* widget) = 0;
};
#define IComponent_iid "com.logos.component.IComponent"
Q_DECLARE_INTERFACE(IComponent, IComponent_iid)

View File

@ -1,27 +1,24 @@
{
"name": "blockchain_ui",
"version": "1.0.0",
"description": "Blockchain UI module for the Logos application",
"author": "Logos Blockchain Team",
"type": "ui",
"main": "blockchain_ui",
"icon": ":/icons/blockchain.png",
"dependencies": ["liblogos_blockchain_module"],
"type": "ui_qml",
"category": "blockchain",
"build": {
"type": "cmake",
"files": [
"src/BlockchainPlugin.cpp",
"src/BlockchainPlugin.h",
"src/BlockchainBackend.cpp",
"src/BlockchainBackend.h",
"src/LogModel.cpp",
"src/LogModel.h",
"src/blockchain_resources.qrc"
]
},
"capabilities": [
"ui_components",
"blockchain"
]
"description": "Blockchain UI module for the Logos application",
"main": "blockchain_ui_plugin",
"icon": "src/icons/blockchain.png",
"view": "qml/BlockchainView.qml",
"dependencies": ["liblogos_blockchain_module"],
"nix": {
"packages": {
"build": [],
"runtime": ["qt6.qtdeclarative", "zstd", "krb5", "abseil-cpp"]
},
"external_libraries": [],
"cmake": {
"find_packages": ["Qt6Gui"],
"extra_sources": [],
"extra_include_dirs": [],
"extra_link_libraries": ["Qt6::Gui"]
}
}
}

View File

@ -1,216 +0,0 @@
# Builds the logos-blockchain-ui-app standalone application
{ pkgs, common, src, logosLiblogos, logosBlockchainModule, logosCapabilityModule, logosBlockchainUI, logosDesignSystem }:
pkgs.stdenv.mkDerivation rec {
pname = "logos-blockchain-ui-app";
version = common.version;
inherit src;
inherit (common) buildInputs meta;
nativeBuildInputs = common.nativeBuildInputs ++ [ pkgs.patchelf pkgs.removeReferencesTo ];
# Provide Qt/GL runtime paths so the wrapper can inject them
qtLibPath = pkgs.lib.makeLibraryPath (
[
pkgs.qt6.qtbase
pkgs.qt6.qtremoteobjects
pkgs.zstd
pkgs.krb5
pkgs.zlib
pkgs.glib
pkgs.stdenv.cc.cc
pkgs.freetype
pkgs.fontconfig
]
++ pkgs.lib.optionals pkgs.stdenv.isLinux [
pkgs.libglvnd
pkgs.mesa.drivers
pkgs.xorg.libX11
pkgs.xorg.libXext
pkgs.xorg.libXrender
pkgs.xorg.libXrandr
pkgs.xorg.libXcursor
pkgs.xorg.libXi
pkgs.xorg.libXfixes
pkgs.xorg.libxcb
]
);
qtPluginPath = "${pkgs.qt6.qtbase}/lib/qt-6/plugins";
qmlImportPath = "${placeholder "out"}/lib:${pkgs.qt6.qtbase}/lib/qt-6/qml";
# This is a GUI application, enable Qt wrapping
dontWrapQtApps = false;
# This is an aggregate runtime layout; avoid stripping to prevent hook errors
dontStrip = true;
# Ensure proper Qt environment setup via wrapper
qtWrapperArgs = [
"--prefix" "LD_LIBRARY_PATH" ":" qtLibPath
"--prefix" "QT_PLUGIN_PATH" ":" qtPluginPath
"--prefix" "QML2_IMPORT_PATH" ":" qmlImportPath
];
preConfigure = ''
runHook prePreConfigure
export MACOSX_DEPLOYMENT_TARGET=12.0
runHook postPreConfigure
'';
# Additional environment variables for Qt and RPATH cleanup
preFixup = ''
runHook prePreFixup
# Set up Qt environment variables
export QT_PLUGIN_PATH="${pkgs.qt6.qtbase}/lib/qt-6/plugins"
export QML_IMPORT_PATH="${pkgs.qt6.qtbase}/lib/qt-6/qml"
# Remove any remaining references to /build/ in binaries and set proper RPATH
find $out -type f -executable -exec sh -c '
if file "$1" | grep -q "ELF.*executable"; then
# Use patchelf to clean up RPATH if it contains /build/
if patchelf --print-rpath "$1" 2>/dev/null | grep -q "/build/"; then
echo "Cleaning RPATH for $1"
patchelf --remove-rpath "$1" 2>/dev/null || true
fi
# Set proper RPATH for the main binary
if echo "$1" | grep -q "/logos-blockchain-ui-app$"; then
echo "Setting RPATH for $1"
patchelf --set-rpath "$out/lib" "$1" 2>/dev/null || true
fi
fi
' _ {} \;
# Also clean up shared libraries
find $out -name "*.so" -exec sh -c '
if patchelf --print-rpath "$1" 2>/dev/null | grep -q "/build/"; then
echo "Cleaning RPATH for $1"
patchelf --remove-rpath "$1" 2>/dev/null || true
fi
' _ {} \;
runHook prePostFixup
'';
configurePhase = ''
runHook preConfigure
echo "Configuring logos-blockchain-ui-app..."
test -d "${logosLiblogos}" || (echo "liblogos not found" && exit 1)
test -d "${logosBlockchainModule}" || (echo "blockchain-module not found" && exit 1)
test -d "${logosCapabilityModule}" || (echo "capability-module not found" && exit 1)
test -d "${logosBlockchainUI}" || (echo "blockchain-ui not found" && exit 1)
test -d "${logosDesignSystem}" || (echo "logos-design-system not found" && exit 1)
cmake -S app -B build \
-GNinja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_OSX_DEPLOYMENT_TARGET=12.0 \
-DCMAKE_INSTALL_RPATH_USE_LINK_PATH=FALSE \
-DCMAKE_INSTALL_RPATH="" \
-DCMAKE_SKIP_BUILD_RPATH=TRUE \
-DLOGOS_LIBLOGOS_ROOT=${logosLiblogos}
runHook postConfigure
'';
buildPhase = ''
runHook preBuild
cmake --build build
echo "logos-blockchain-ui-app built successfully!"
runHook postBuild
'';
installPhase = ''
runHook preInstall
# Create output directories
mkdir -p $out/bin $out/lib $out/modules
# Install our app binary
if [ -f "build/bin/logos-blockchain-ui-app" ]; then
cp build/bin/logos-blockchain-ui-app "$out/bin/"
echo "Installed logos-blockchain-ui-app binary"
fi
# Copy the core binaries from liblogos
if [ -f "${logosLiblogos}/bin/logoscore" ]; then
cp -L "${logosLiblogos}/bin/logoscore" "$out/bin/"
echo "Installed logoscore binary"
fi
if [ -f "${logosLiblogos}/bin/logos_host" ]; then
cp -L "${logosLiblogos}/bin/logos_host" "$out/bin/"
echo "Installed logos_host binary"
fi
# Copy required shared libraries from liblogos
if ls "${logosLiblogos}/lib/"liblogos_core.* >/dev/null 2>&1; then
cp -L "${logosLiblogos}/lib/"liblogos_core.* "$out/lib/" || true
fi
# Determine platform-specific plugin extension
OS_EXT="so"
case "$(uname -s)" in
Darwin) OS_EXT="dylib";;
Linux) OS_EXT="so";;
MINGW*|MSYS*|CYGWIN*) OS_EXT="dll";;
esac
# Copy module plugins into the modules directory
if [ -f "${logosCapabilityModule}/lib/capability_module_plugin.$OS_EXT" ]; then
cp -L "${logosCapabilityModule}/lib/capability_module_plugin.$OS_EXT" "$out/modules/"
fi
if [ -f "${logosBlockchainModule}/lib/liblogos_blockchain_module.$OS_EXT" ]; then
cp -L "${logosBlockchainModule}/lib/liblogos_blockchain_module.$OS_EXT" "$out/modules/"
fi
# Copy liblogos_blockchain library to modules directory (needed by blockchain module)
if [ -f "${logosBlockchainModule}/lib/liblogos_blockchain.$OS_EXT" ]; then
cp -L "${logosBlockchainModule}/lib/liblogos_blockchain.$OS_EXT" "$out/modules/"
fi
# Copy circuits from blockchain module to lib (needed at runtime)
if [ -d "${logosBlockchainModule}/share/circuits" ]; then
cp -r "${logosBlockchainModule}/share/circuits" "$out/modules/"
fi
# Copy blockchain_ui Qt plugin to root directory (not modules, as it's loaded differently)
if [ -f "${logosBlockchainUI}/lib/blockchain_ui.$OS_EXT" ]; then
cp -L "${logosBlockchainUI}/lib/blockchain_ui.$OS_EXT" "$out/"
fi
# Copy design system QML modules (Logos.Theme, Logos.Controls) for runtime
if [ -d "${logosDesignSystem}/lib/Logos/Theme" ]; then
mkdir -p "$out/lib/Logos"
cp -R "${logosDesignSystem}/lib/Logos/Theme" "$out/lib/Logos/"
echo "Copied Logos.Theme to lib/Logos/Theme/"
fi
if [ -d "${logosDesignSystem}/lib/Logos/Controls" ]; then
mkdir -p "$out/lib/Logos"
cp -R "${logosDesignSystem}/lib/Logos/Controls" "$out/lib/Logos/"
echo "Copied Logos.Controls to lib/Logos/Controls/"
fi
cat > $out/README.txt <<EOF
Logos Blockchain UI App
=======================
liblogos: ${logosLiblogos}
blockchain-module: ${logosBlockchainModule}
capability-module: ${logosCapabilityModule}
blockchain-ui: ${logosBlockchainUI}
design-system: ${logosDesignSystem}
Layout:
bin/logos-blockchain-ui-app
lib/
modules/
blockchain_ui.$OS_EXT
EOF
runHook postInstall
'';
}

View File

@ -1,41 +0,0 @@
# Common build configuration shared across all packages
{ pkgs, logosSdk, logosLiblogos }:
{
pname = "logos-blockchain-ui";
version = "1.0.0";
# Common native build inputs
nativeBuildInputs = [
pkgs.cmake
pkgs.ninja
pkgs.pkg-config
pkgs.qt6.wrapQtAppsHook
];
# Common runtime dependencies
buildInputs = [
pkgs.qt6.qtbase
pkgs.qt6.qtremoteobjects
pkgs.zstd
pkgs.krb5
pkgs.abseil-cpp
];
# Common CMake flags
cmakeFlags = [
"-GNinja"
"-DLOGOS_CPP_SDK_ROOT=${logosSdk}"
"-DLOGOS_LIBLOGOS_ROOT=${logosLiblogos}"
];
env = {
LOGOS_LIBLOGOS_ROOT = "${logosLiblogos}";
};
# Metadata
meta = with pkgs.lib; {
description = "Logos Blockchain UI - A Qt UI plugin for Logos Blockchain Module";
platforms = platforms.unix;
};
}

View File

@ -1,51 +0,0 @@
# Builds the logos-blockchain-ui library
{ pkgs, common, src, logosBlockchainModule }:
pkgs.stdenv.mkDerivation {
pname = "${common.pname}-lib";
version = common.version;
inherit src;
inherit (common) buildInputs cmakeFlags meta env;
nativeBuildInputs = common.nativeBuildInputs;
# Library (Qt plugin), not an app — no Qt wrapper
dontWrapQtApps = true;
configurePhase = ''
runHook preConfigure
cmake -S . -B build \
-GNinja \
-DCMAKE_BUILD_TYPE=Release \
''${cmakeFlags}
runHook postConfigure
'';
buildPhase = ''
runHook preBuild
cmake --build build
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p $out/lib
if [ -f build/modules/blockchain_ui.dylib ]; then
cp build/modules/blockchain_ui.dylib $out/lib/
elif [ -f build/modules/blockchain_ui.so ]; then
cp build/modules/blockchain_ui.so $out/lib/
else
echo "Error: No library file found in build/modules/"
ls -la build/modules/ 2>/dev/null || true
exit 1
fi
# Copy circuits from blockchain module so result/lib/circuits is available
if [ -d "${logosBlockchainModule}/share/circuits" ]; then
cp -r "${logosBlockchainModule}/share/circuits" $out/modules/
fi
runHook postInstall
'';
}

View File

@ -1,154 +1,135 @@
#include "BlockchainBackend.h"
#include "logos_api.h"
#include "logos_api_client.h"
#include <QByteArray>
#include <QClipboard>
#include <QDebug>
#include <QDateTime>
#include <QDebug>
#include <QDir>
#include <QGuiApplication>
#include <QJsonDocument>
#include <QJsonObject>
#include <QModelIndex>
#include <QSettings>
#include <QSignalBlocker>
#include <QTimer>
#include <QUrl>
#include <QVariant>
namespace {
const char SETTINGS_ORG[] = "Logos";
const char SETTINGS_APP[] = "BlockchainUI";
const char USER_CONFIG_KEY[] = "userConfigPath";
const char DEPLOYMENT_CONFIG_KEY[] = "deploymentConfigPath";
const QString BLOCKCHAIN_MODULE_NAME = QStringLiteral("liblogos_blockchain_module");
const QString BlockchainBackend::BLOCKCHAIN_MODULE_NAME =
QStringLiteral("liblogos_blockchain_module");
static QString toLocalPath(const QString& pathInput)
{
if (pathInput.trimmed().isEmpty())
return pathInput;
return QUrl::fromUserInput(pathInput).toLocalFile();
}
BlockchainBackend::BlockchainBackend(LogosAPI* logosAPI, QObject* parent)
: QObject(parent),
m_status(NotStarted),
m_userConfig(""),
m_deploymentConfig(""),
m_logModel(new LogModel(this)),
m_accountsModel(new AccountsModel(this)),
m_logosAPI(nullptr),
m_blockchainClient(nullptr)
: BlockchainBackendSimpleSource(parent)
, m_logosAPI(logosAPI)
, m_accountsModel(new AccountsModel(this))
, m_logModel(new LogModel(this))
{
QSettings s(SETTINGS_ORG, SETTINGS_APP);
const QString envConfigPath = QString::fromUtf8(qgetenv("LB_CONFIG_PATH"));
const QString savedUserConfig = s.value(USER_CONFIG_KEY).toString();
const QString savedDeploymentConfig = s.value(DEPLOYMENT_CONFIG_KEY).toString();
setStatus(NotStarted);
setUseGeneratedConfig(false);
setGeneratedUserConfigPath(
QDir::currentPath() + QStringLiteral("/user_config.yaml"));
if (!envConfigPath.isEmpty()) {
m_userConfig = envConfigPath;
} else if (!savedUserConfig.isEmpty()) {
m_userConfig = savedUserConfig;
}
if (!savedDeploymentConfig.isEmpty()) {
m_deploymentConfig = savedDeploymentConfig;
}
// Restore saved config paths
QSettings s("Logos", "BlockchainUI");
const QString envConfigPath =
QString::fromUtf8(qgetenv("LB_CONFIG_PATH"));
const QString savedUserConfig =
s.value("userConfigPath").toString();
const QString savedDeploymentConfig =
s.value("deploymentConfigPath").toString();
if (!logosAPI) {
logosAPI = new LogosAPI("blockchain_ui", this);
}
if (!envConfigPath.isEmpty())
setUserConfig(toLocalPath(envConfigPath));
else if (!savedUserConfig.isEmpty())
setUserConfig(toLocalPath(savedUserConfig));
m_logosAPI = logosAPI;
m_blockchainClient = m_logosAPI->getClient(BLOCKCHAIN_MODULE_NAME);
if (!savedDeploymentConfig.isEmpty())
setDeploymentConfig(toLocalPath(savedDeploymentConfig));
if (!m_blockchainClient) {
setStatus(ErrorNotInitialized);
// Re-apply pre-.rep behavior: normalize file URLs, then persist (as master did in setters).
connect(this, &BlockchainBackendSimpleSource::userConfigChanged, this, [this]() {
const QString p = userConfig();
const QString n = toLocalPath(p);
if (n != p) {
QSignalBlocker b(this);
setUserConfig(n);
}
QSettings("Logos", "BlockchainUI")
.setValue("userConfigPath", userConfig());
});
connect(this, &BlockchainBackendSimpleSource::deploymentConfigChanged, this, [this]() {
const QString p = deploymentConfig();
const QString n = toLocalPath(p);
if (n != p) {
QSignalBlocker b(this);
setDeploymentConfig(n);
}
QSettings("Logos", "BlockchainUI")
.setValue("deploymentConfigPath", deploymentConfig());
});
if (!m_logosAPI) {
qWarning() << "BlockchainBackend: constructed without LogosAPI";
return;
}
QObject* replica = m_blockchainClient->requestObject(BLOCKCHAIN_MODULE_NAME);
if (replica) {
replica->setParent(this);
m_blockchainClient->onEvent(replica, this, "newBlock", [this](const QString&, const QVariantList& data) {
onNewBlock(data);
});
} else {
setStatus(ErrorSubscribeFailed);
m_blockchainClient = m_logosAPI->getClient(BLOCKCHAIN_MODULE_NAME);
if (!m_blockchainClient) {
setStatus(ErrorNotInitialized);
qWarning() << "BlockchainBackend: failed to get blockchain module client";
return;
}
// NOTE: do NOT call requestObject() here. ui-host invokes initLogos()
// (and therefore this constructor) synchronously via Qt::DirectConnection
// and only signals "READY" once it returns. requestObject() blocks for up
// to its 20s timeout when the backend module isn't running yet — which is
// the normal case, since the node is started later from this UI. Blocking
// here makes ui-host miss its readiness deadline, so the host kills it and
// the whole view fails to load. The newBlock subscription is only
// meaningful once the node is running, so it is deferred to
// subscribeToBlockEvents(), called after a successful startBlockchain().
qDebug() << "BlockchainBackend: initialized";
}
void BlockchainBackend::subscribeToBlockEvents()
{
if (m_blockEventsSubscribed || !m_blockchainClient)
return;
LogosObject* replica =
m_blockchainClient->requestObject(BLOCKCHAIN_MODULE_NAME);
if (!replica)
return;
m_blockchainClient->onEvent(
replica, "newBlock",
[this](const QString&, const QVariantList& data) {
const QString timestamp =
QDateTime::currentDateTime().toString("HH:mm:ss");
QString line;
if (!data.isEmpty())
line = QString("[%1] New block: %2")
.arg(timestamp, data.first().toString());
else
line = QString("[%1] New block (no data)").arg(timestamp);
m_logModel->append(line);
});
m_blockEventsSubscribed = true;
}
BlockchainBackend::~BlockchainBackend()
{
stopBlockchain();
}
void BlockchainBackend::setStatus(BlockchainStatus newStatus)
{
if (m_status != newStatus) {
m_status = newStatus;
emit statusChanged();
}
}
void BlockchainBackend::setUserConfig(const QString& path)
{
const QString localPath = QUrl::fromUserInput(path).toLocalFile();
if (m_userConfig != localPath) {
m_userConfig = localPath;
QSettings s(SETTINGS_ORG, SETTINGS_APP);
s.setValue(USER_CONFIG_KEY, m_userConfig);
emit userConfigChanged();
}
}
void BlockchainBackend::setDeploymentConfig(const QString& path)
{
const QString localPath = QUrl::fromUserInput(path).toLocalFile();
if (m_deploymentConfig != localPath) {
m_deploymentConfig = localPath;
QSettings s(SETTINGS_ORG, SETTINGS_APP);
s.setValue(DEPLOYMENT_CONFIG_KEY, m_deploymentConfig);
emit deploymentConfigChanged();
}
}
void BlockchainBackend::setUseGeneratedConfig(bool useGenerated)
{
if (m_useGeneratedConfig != useGenerated) {
m_useGeneratedConfig = useGenerated;
emit useGeneratedConfigChanged();
}
}
void BlockchainBackend::clearLogs()
{
m_logModel->clear();
}
void BlockchainBackend::copyToClipboard(const QString& text)
{
if (QGuiApplication::clipboard())
QGuiApplication::clipboard()->setText(text);
}
QString BlockchainBackend::getBalance(const QString& addressHex)
{
QString result;
if (!m_blockchainClient) {
result = QStringLiteral("Error: Module not initialized.");
} else {
QVariant v = m_blockchainClient->invokeRemoteMethod(BLOCKCHAIN_MODULE_NAME, "wallet_get_balance", addressHex);
result = v.isValid() ? v.toString() : QStringLiteral("Error: Call failed.");
}
m_accountsModel->setBalanceForAddress(addressHex, result);
return result;
}
QString BlockchainBackend::transferFunds(const QString& fromKeyHex, const QString& toKeyHex, const QString& amountStr)
{
if (!m_blockchainClient) {
return QStringLiteral("Error: Module not initialized.");
}
QVariant result = m_blockchainClient->invokeRemoteMethod(
BLOCKCHAIN_MODULE_NAME,
"wallet_transfer_funds",
fromKeyHex,
fromKeyHex,
toKeyHex,
amountStr,
QString());
return result.isValid() ? result.toString() : QStringLiteral("Error: Call failed.");
if (status() == Running || status() == Starting)
stopBlockchain();
}
void BlockchainBackend::startBlockchain()
@ -161,48 +142,24 @@ void BlockchainBackend::startBlockchain()
setStatus(Starting);
QVariant result = m_blockchainClient->invokeRemoteMethod(
BLOCKCHAIN_MODULE_NAME, "start", m_userConfig, m_deploymentConfig);
BLOCKCHAIN_MODULE_NAME, "start", userConfig(), deploymentConfig());
int resultCode = result.isValid() ? result.toInt() : -1;
if (resultCode == 0 || resultCode == 1) {
setStatus(Running);
subscribeToBlockEvents();
QTimer::singleShot(500, this, [this]() { refreshAccounts(); });
} else if (resultCode == 2) {
setStatus(ErrorConfigMissing);
} else if (resultCode == 3) {
setStatus(ErrorStartFailed);
} else {
setStatus(ErrorStartFailed);
}
}
void BlockchainBackend::refreshAccounts()
{
if (!m_blockchainClient) return;
QVariant result = m_blockchainClient->invokeRemoteMethod(BLOCKCHAIN_MODULE_NAME, "wallet_get_known_addresses");
QStringList list = result.isValid() && result.canConvert<QStringList>() ? result.toStringList() : QStringList();
qDebug() << "BlockchainBackend: received from blockchain lib: type=QStringList, count=" << list.size();
m_accountsModel->setAddresses(list);
QTimer::singleShot(0, this, [this, list]() { fetchBalancesForAccounts(list); });
}
void BlockchainBackend::fetchBalancesForAccounts(const QStringList& list)
{
if (!m_blockchainClient) return;
const int n = list.size();
for (int i = 0; i < n; ++i) {
const QString address = list[i];
if (address.isEmpty()) continue;
const QString balance = getBalance(address);
m_accountsModel->setBalanceForAddress(address, balance);
}
}
void BlockchainBackend::stopBlockchain()
{
if (m_status != Running && m_status != Starting) {
if (status() != Running && status() != Starting)
return;
}
if (!m_blockchainClient) {
setStatus(ErrorNotInitialized);
@ -211,60 +168,87 @@ void BlockchainBackend::stopBlockchain()
setStatus(Stopping);
QVariant result = m_blockchainClient->invokeRemoteMethod(BLOCKCHAIN_MODULE_NAME, "stop");
QVariant result = m_blockchainClient->invokeRemoteMethod(
BLOCKCHAIN_MODULE_NAME, "stop");
int resultCode = result.isValid() ? result.toInt() : -1;
if (resultCode == 0 || resultCode == 1) {
if (resultCode == 0 || resultCode == 1)
setStatus(Stopped);
} else {
else
setStatus(ErrorStopFailed);
}
void BlockchainBackend::refreshAccounts()
{
if (!m_blockchainClient) return;
QVariant result = m_blockchainClient->invokeRemoteMethod(
BLOCKCHAIN_MODULE_NAME, "wallet_get_known_addresses");
QStringList list =
result.isValid() && result.canConvert<QStringList>()
? result.toStringList()
: QStringList();
m_accountsModel->setAddresses(list);
QTimer::singleShot(0, this,
[this, list]() { fetchBalancesForAccounts(list); });
}
void BlockchainBackend::fetchBalancesForAccounts(const QStringList& list)
{
if (!m_blockchainClient) return;
for (const QString& address : list) {
if (address.isEmpty()) continue;
getBalance(address);
}
}
void BlockchainBackend::onNewBlock(const QVariantList& data)
{
QString timestamp = QDateTime::currentDateTime().toString("HH:mm:ss");
QString line;
if (!data.isEmpty()) {
QString blockInfo = data.first().toString();
line = QString("[%1] 📦 New block: %2").arg(timestamp, blockInfo);
} else {
line = QString("[%1] 📦 New block (no data)").arg(timestamp);
}
m_logModel->append(line);
}
static QString toLocalPath(const QString& pathInput)
{
if (pathInput.trimmed().isEmpty())
return pathInput;
return QUrl::fromUserInput(pathInput).toLocalFile();
}
int BlockchainBackend::generateConfig(const QString& outputPath,
const QStringList& initialPeers,
int netPort,
int blendPort,
const QString& httpAddr,
const QString& externalAddress,
bool noPublicIpCheck,
int deploymentMode,
const QString& deploymentConfigPath,
const QString& statePath)
QString BlockchainBackend::getBalance(QString addressHex)
{
QString result;
if (!m_blockchainClient) {
return -1;
result = QStringLiteral("Error: Module not initialized.");
} else {
QVariant v = m_blockchainClient->invokeRemoteMethod(
BLOCKCHAIN_MODULE_NAME, "wallet_get_balance", addressHex);
result = v.isValid() ? v.toString()
: QStringLiteral("Error: Call failed.");
}
m_accountsModel->setBalanceForAddress(addressHex, result);
return result;
}
QString BlockchainBackend::transferFunds(
QString fromKeyHex, QString toKeyHex, QString amountStr)
{
if (!m_blockchainClient)
return QStringLiteral("Error: Module not initialized.");
QVariant result = m_blockchainClient->invokeRemoteMethod(
BLOCKCHAIN_MODULE_NAME, "wallet_transfer_funds",
fromKeyHex, fromKeyHex, toKeyHex, amountStr, QString());
return result.isValid() ? result.toString()
: QStringLiteral("Error: Call failed.");
}
int BlockchainBackend::generateConfig(
QString outputPath, QStringList initialPeers, int netPort, int blendPort,
QString httpAddr, QString externalAddress, bool noPublicIpCheck,
int deploymentMode, QString deploymentConfigPath, QString statePath)
{
if (!m_blockchainClient)
return -1;
QVariantMap normalized;
// Output path: default if empty, then normalize
QString out = outputPath.trimmed();
if (out.isEmpty()) {
if (out.isEmpty())
out = generatedUserConfigPath();
} else {
else
out = toLocalPath(out);
}
normalized.insert(QStringLiteral("output"), out);
normalized.insert("output", out);
if (!initialPeers.isEmpty()) {
QVariantList peersList;
@ -273,40 +257,48 @@ int BlockchainBackend::generateConfig(const QString& outputPath,
peersList.append(p.trimmed());
}
if (!peersList.isEmpty())
normalized.insert(QStringLiteral("initial_peers"), peersList);
normalized.insert("initial_peers", peersList);
}
if (netPort > 0)
normalized.insert(QStringLiteral("net_port"), netPort);
normalized.insert("net_port", netPort);
if (blendPort > 0)
normalized.insert(QStringLiteral("blend_port"), blendPort);
normalized.insert("blend_port", blendPort);
if (!httpAddr.trimmed().isEmpty())
normalized.insert(QStringLiteral("http_addr"), httpAddr.trimmed());
normalized.insert("http_addr", httpAddr.trimmed());
if (!externalAddress.trimmed().isEmpty())
normalized.insert(QStringLiteral("external_address"), externalAddress.trimmed());
normalized.insert("external_address", externalAddress.trimmed());
if (noPublicIpCheck)
normalized.insert(QStringLiteral("no_public_ip_check"), true);
normalized.insert("no_public_ip_check", true);
if (deploymentMode == 0) {
QVariantMap deployment;
deployment.insert(QStringLiteral("well_known_deployment"), QStringLiteral("devnet"));
normalized.insert(QStringLiteral("deployment"), deployment);
} else if (deploymentMode == 1 && !deploymentConfigPath.trimmed().isEmpty()) {
deployment.insert("well_known_deployment", "devnet");
normalized.insert("deployment", deployment);
} else if (deploymentMode == 1
&& !deploymentConfigPath.trimmed().isEmpty()) {
QVariantMap deployment;
deployment.insert(QStringLiteral("config_path"), toLocalPath(deploymentConfigPath.trimmed()));
normalized.insert(QStringLiteral("deployment"), deployment);
deployment.insert("config_path",
toLocalPath(deploymentConfigPath.trimmed()));
normalized.insert("deployment", deployment);
}
if (!statePath.trimmed().isEmpty())
normalized.insert(QStringLiteral("state_path"), toLocalPath(statePath.trimmed()));
normalized.insert("state_path", toLocalPath(statePath.trimmed()));
const QJsonDocument doc = QJsonDocument::fromVariant(normalized);
const QByteArray jsonBytes = doc.toJson(QJsonDocument::Compact);
const QString jsonToSend = QString::fromUtf8(jsonBytes);
const QString jsonToSend =
QString::fromUtf8(doc.toJson(QJsonDocument::Compact));
QVariant result = m_blockchainClient->invokeRemoteMethod(
BLOCKCHAIN_MODULE_NAME, "generate_user_config_from_str", jsonToSend);
return result.isValid() ? result.toInt() : -1;
}
QString BlockchainBackend::generatedUserConfigPath() const
void BlockchainBackend::clearLogs()
{
return QDir::currentPath() + QStringLiteral("/user_config.yaml");
m_logModel->clear();
}
void BlockchainBackend::copyToClipboard(QString text)
{
if (QGuiApplication::clipboard())
QGuiApplication::clipboard()->setText(text);
}

View File

@ -1,96 +1,70 @@
#pragma once
#ifndef BLOCKCHAIN_BACKEND_H
#define BLOCKCHAIN_BACKEND_H
#include <QObject>
#include <QString>
#include <QStringList>
#include <QVariant>
#include "logos_api.h"
#include "logos_api_client.h"
#include <QVariantList>
#include "rep_BlockchainBackend_source.h"
#include "AccountsModel.h"
#include "LogModel.h"
class BlockchainBackend : public QObject {
class LogosAPI;
class LogosAPIClient;
// Source-side implementation of the BlockchainBackend .rep interface.
//
// Inheriting from BlockchainBackendSimpleSource gives us the generated PROPs,
// SLOTs and SIGNALs from BlockchainBackend.rep.
//
// AccountsModel* / LogModel* are subclass-only Q_PROPERTYs — QAbstractItemModel*
// can't flow through a .rep, so ui-host auto-remotes each such property as
// "<module>/<propertyName>" (see logos-view-module-runtime/ui-host/main.cpp).
// QML acquires them via logos.model("blockchain_ui", "accounts"|"logs").
class BlockchainBackend : public BlockchainBackendSimpleSource
{
Q_OBJECT
Q_PROPERTY(AccountsModel* accounts READ accounts CONSTANT)
Q_PROPERTY(LogModel* logs READ logs CONSTANT)
public:
enum BlockchainStatus {
NotStarted = 0,
Starting,
Running,
Stopping,
Stopped,
Error,
ErrorNotInitialized,
ErrorConfigMissing,
ErrorStartFailed,
ErrorStopFailed,
ErrorSubscribeFailed
};
Q_ENUM(BlockchainStatus)
explicit BlockchainBackend(LogosAPI* logosAPI, QObject* parent = nullptr);
~BlockchainBackend() override;
Q_PROPERTY(BlockchainStatus status READ status NOTIFY statusChanged)
Q_PROPERTY(QString userConfig READ userConfig WRITE setUserConfig NOTIFY userConfigChanged)
Q_PROPERTY(QString deploymentConfig READ deploymentConfig WRITE setDeploymentConfig NOTIFY deploymentConfigChanged)
Q_PROPERTY(bool useGeneratedConfig READ useGeneratedConfig WRITE setUseGeneratedConfig NOTIFY useGeneratedConfigChanged)
Q_PROPERTY(LogModel* logModel READ logModel CONSTANT)
Q_PROPERTY(AccountsModel* accountsModel READ accountsModel CONSTANT)
Q_PROPERTY(QString generatedUserConfigPath READ generatedUserConfigPath CONSTANT)
explicit BlockchainBackend(LogosAPI* logosAPI = nullptr, QObject* parent = nullptr);
~BlockchainBackend();
BlockchainStatus status() const { return m_status; }
QString userConfig() const { return m_userConfig; }
QString deploymentConfig() const { return m_deploymentConfig; }
bool useGeneratedConfig() const { return m_useGeneratedConfig; }
LogModel* logModel() const { return m_logModel; }
AccountsModel* accountsModel() const { return m_accountsModel; }
void setUserConfig(const QString& path);
void setDeploymentConfig(const QString& path);
void setUseGeneratedConfig(bool useGenerated);
Q_INVOKABLE void clearLogs();
Q_INVOKABLE void copyToClipboard(const QString& text);
Q_INVOKABLE QString getBalance(const QString& addressHex);
Q_INVOKABLE QString transferFunds(
const QString& fromKeyHex,
const QString& toKeyHex,
const QString& amountStr);
Q_INVOKABLE void startBlockchain();
Q_INVOKABLE void stopBlockchain();
Q_INVOKABLE void refreshAccounts();
Q_INVOKABLE int generateConfig(const QString& outputPath,
const QStringList& initialPeers,
int netPort,
int blendPort,
const QString& httpAddr,
const QString& externalAddress,
bool noPublicIpCheck,
int deploymentMode,
const QString& deploymentConfigPath,
const QString& statePath);
Q_INVOKABLE QString generatedUserConfigPath() const;
AccountsModel* accounts() const { return m_accountsModel; }
LogModel* logs() const { return m_logModel; }
public slots:
void onNewBlock(const QVariantList& data);
signals:
void statusChanged();
void userConfigChanged();
void deploymentConfigChanged();
void useGeneratedConfigChanged();
// Overrides of the pure-virtual slots generated from the .rep.
void startBlockchain() override;
void stopBlockchain() override;
void refreshAccounts() override;
QString getBalance(QString addressHex) override;
QString transferFunds(QString fromKeyHex, QString toKeyHex, QString amountStr) override;
int generateConfig(QString outputPath, QStringList initialPeers, int netPort,
int blendPort, QString httpAddr, QString externalAddress,
bool noPublicIpCheck, int deploymentMode,
QString deploymentConfigPath, QString statePath) override;
void clearLogs() override;
void copyToClipboard(QString text) override;
private:
void setStatus(BlockchainStatus newStatus);
void fetchBalancesForAccounts(const QStringList& list);
// Subscribes to the backend "newBlock" event. Deferred out of the
// constructor because requestObject() blocks until the backend module is
// running; calling it during the synchronous initLogos() would stall
// ui-host past its readiness deadline and the view would fail to load.
void subscribeToBlockEvents();
BlockchainStatus m_status;
QString m_userConfig;
QString m_deploymentConfig;
bool m_useGeneratedConfig = false;
LogModel* m_logModel;
AccountsModel* m_accountsModel;
LogosAPI* m_logosAPI = nullptr;
LogosAPIClient* m_blockchainClient = nullptr;
AccountsModel* m_accountsModel = nullptr;
LogModel* m_logModel = nullptr;
bool m_blockEventsSubscribed = false;
LogosAPI* m_logosAPI;
LogosAPIClient* m_blockchainClient;
static const QString BLOCKCHAIN_MODULE_NAME;
};
#endif // BLOCKCHAIN_BACKEND_H

19
src/BlockchainBackend.rep Normal file
View File

@ -0,0 +1,19 @@
class BlockchainBackend
{
ENUM BlockchainStatus { NotStarted=0, Starting=1, Running=2, Stopping=3, Stopped=4, Error=5, ErrorNotInitialized=6, ErrorConfigMissing=7, ErrorStartFailed=8, ErrorStopFailed=9, ErrorSubscribeFailed=10 }
PROP(BlockchainStatus status READONLY)
PROP(QString userConfig READWRITE)
PROP(QString deploymentConfig READWRITE)
PROP(bool useGeneratedConfig READWRITE)
PROP(QString generatedUserConfigPath READONLY)
SLOT(void startBlockchain())
SLOT(void stopBlockchain())
SLOT(void refreshAccounts())
SLOT(QString getBalance(QString addressHex))
SLOT(QString transferFunds(QString fromKeyHex, QString toKeyHex, QString amountStr))
SLOT(int generateConfig(QString outputPath, QStringList initialPeers, int netPort, int blendPort, QString httpAddr, QString externalAddress, bool noPublicIpCheck, int deploymentMode, QString deploymentConfigPath, QString statePath))
SLOT(void clearLogs())
SLOT(void copyToClipboard(QString text))
}

View File

@ -1,54 +1,19 @@
#include "BlockchainPlugin.h"
#include "BlockchainBackend.h"
#include "LogModel.h"
#include <QQuickWidget>
#include <QQmlContext>
#include <QQmlEngine>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QUrl>
QWidget* BlockchainPlugin::createWidget(LogosAPI* logosAPI) {
qDebug() << "BlockchainPlugin::createWidget called";
QQuickWidget* quickWidget = new QQuickWidget();
quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
qmlRegisterType<BlockchainBackend>("BlockchainBackend", 1, 0, "BlockchainBackend");
qmlRegisterType<LogModel>("BlockchainBackend", 1, 0, "LogModel");
BlockchainBackend* backend = new BlockchainBackend(logosAPI, quickWidget);
quickWidget->rootContext()->setContextProperty("backend", backend);
QString qmlSource = "qrc:/qml/BlockchainView.qml";
QString importPath = "qrc:/qml";
QString envPath = QString::fromUtf8(qgetenv("BLOCKCHAIN_UI_QML_PATH")).trimmed();
if (!envPath.isEmpty()) {
QFileInfo info(envPath);
if (info.isDir()) {
QString main = QDir(info.absoluteFilePath()).absoluteFilePath("BlockchainView.qml");
if (QFile::exists(main)) {
importPath = info.absoluteFilePath();
qmlSource = QUrl::fromLocalFile(main).toString();
} else {
qWarning() << "BLOCKCHAIN_UI_QML_PATH: BlockchainView.qml not found in" << info.absoluteFilePath();
}
}
}
quickWidget->engine()->addImportPath(importPath);
quickWidget->setSource(QUrl(qmlSource));
if (quickWidget->status() == QQuickWidget::Error) {
qWarning() << "BlockchainPlugin: Failed to load QML:" << quickWidget->errors();
}
return quickWidget;
BlockchainPlugin::BlockchainPlugin(QObject* parent)
: QObject(parent)
{
}
void BlockchainPlugin::destroyWidget(QWidget* widget) {
delete widget;
BlockchainPlugin::~BlockchainPlugin() = default;
void BlockchainPlugin::initLogos(LogosAPI* api)
{
if (m_backend) return;
m_backend = new BlockchainBackend(api, this);
setBackend(m_backend);
qDebug() << "BlockchainPlugin: backend initialized";
}

View File

@ -1,14 +1,38 @@
#pragma once
#ifndef BLOCKCHAIN_PLUGIN_H
#define BLOCKCHAIN_PLUGIN_H
#include <IComponent.h>
#include <QObject>
#include <QString>
#include <QtPlugin> // for Q_PLUGIN_METADATA, Q_INTERFACES
#include "BlockchainPluginInterface.h"
#include "LogosViewPluginBase.h"
class BlockchainPlugin : public QObject, public IComponent {
class LogosAPI;
class BlockchainBackend;
// Thin plugin entry point. Holds a BlockchainBackend and lets the
// generated view-plugin base expose it to ui-host.
class BlockchainPlugin : public QObject,
public BlockchainPluginInterface,
public BlockchainBackendViewPluginBase
{
Q_OBJECT
Q_INTERFACES(IComponent)
Q_PLUGIN_METADATA(IID IComponent_iid FILE "metadata.json")
Q_PLUGIN_METADATA(IID BlockchainPluginInterface_iid FILE "../metadata.json")
Q_INTERFACES(BlockchainPluginInterface)
public:
Q_INVOKABLE QWidget* createWidget(LogosAPI* logosAPI = nullptr) override;
void destroyWidget(QWidget* widget) override;
explicit BlockchainPlugin(QObject* parent = nullptr);
~BlockchainPlugin() override;
QString name() const override { return "blockchain_ui"; }
QString version() const override { return "1.0.0"; }
// Called by ui-host after plugin load. Creates the backend and wires
// it up with the provided LogosAPI.
Q_INVOKABLE void initLogos(LogosAPI* api);
private:
BlockchainBackend* m_backend = nullptr;
};
#endif // BLOCKCHAIN_PLUGIN_H

View File

@ -0,0 +1,19 @@
#ifndef BLOCKCHAIN_PLUGIN_INTERFACE_H
#define BLOCKCHAIN_PLUGIN_INTERFACE_H
#include <QtPlugin> // for Q_DECLARE_INTERFACE
#include "interface.h"
// Marker interface used by Qt's plugin loader to identify the blockchain UI
// plugin. The actual API surface (Q_INVOKABLE methods, properties, signals)
// lives in BlockchainBackend.rep — this header only carries the IID.
class BlockchainPluginInterface : public PluginInterface
{
public:
virtual ~BlockchainPluginInterface() = default;
};
#define BlockchainPluginInterface_iid "org.logos.BlockchainPluginInterface"
Q_DECLARE_INTERFACE(BlockchainPluginInterface, BlockchainPluginInterface_iid)
#endif // BLOCKCHAIN_PLUGIN_INTERFACE_H

View File

@ -1,18 +0,0 @@
<RCC>
<qresource prefix="/">
<file>qml/BlockchainView.qml</file>
<file>qml/views/qmldir</file>
<file>qml/views/StatusConfigView.qml</file>
<file>qml/views/LogsView.qml</file>
<file>qml/views/WalletView.qml</file>
<file>qml/views/GenerateConfigView.qml</file>
<file>qml/views/ConfigChoiceView.qml</file>
<file>qml/views/SetConfigPathView.qml</file>
<file>qml/controls/AccountDelegate.qml</file>
<file>qml/controls/LogosCopyButton.qml</file>
<file>icons/blockchain.png</file>
<file>icons/copy.svg</file>
<file>icons/checkmark.svg</file>
<file>icons/refresh.svg</file>
</qresource>
</RCC>

View File

@ -2,91 +2,160 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import BlockchainBackend
import Logos.Theme
// BlockchainStatus enum (NotStarted/Starting/Running/.../ErrorSubscribeFailed)
// declared in BlockchainBackend.rep registered with QML by the replica
// factory plugin.
import Logos.BlockchainBackend 1.0
import "views"
Rectangle {
id: root
readonly property var backend: logos.module("blockchain_ui")
// `ready` can't be a binding on logos.isViewModuleReady(): that's a
// Q_INVOKABLE method, not a Q_PROPERTY, so the binding wouldn't refresh
// when the replica transitions to Valid. Drive it from the bridge's
// viewModuleReadyChanged signal instead.
property bool ready: false
Connections {
target: logos
function onViewModuleReadyChanged(moduleName, isReady) {
if (moduleName === "blockchain_ui")
root.ready = isReady && root.backend !== null
}
}
Component.onCompleted: {
// Cover the case where the replica is already Valid by the time
// we attach the Connections handler.
root.ready = root.backend !== null && logos.isViewModuleReady("blockchain_ui")
}
// Models live on the C++ backend and are auto-remoted by ui-host as
// "<module>/<propertyName>". QML acquires them via logos.model(...).
readonly property var accountsModel: logos.model("blockchain_ui", "accounts")
readonly property var logModel: logos.model("blockchain_ui", "logs")
QtObject {
id: _d
function getStatusString(status) {
switch(status) {
case BlockchainBackend.NotStarted: return qsTr("Not Started");
case BlockchainBackend.Starting: return qsTr("Starting...");
case BlockchainBackend.Running: return qsTr("Running");
case BlockchainBackend.Stopping: return qsTr("Stopping...");
case BlockchainBackend.Stopped: return qsTr("Stopped");
case BlockchainBackend.Error: return qsTr("Error");
case BlockchainBackend.ErrorNotInitialized: return qsTr("Error: Module not initialized");
case BlockchainBackend.ErrorConfigMissing: return qsTr("Error: Config path missing");
case BlockchainBackend.ErrorStartFailed: return qsTr("Error: Failed to start node");
case BlockchainBackend.ErrorStopFailed: return qsTr("Error: Failed to stop node");
case BlockchainBackend.ErrorSubscribeFailed: return qsTr("Error: Failed to subscribe to events");
default: return qsTr("Unknown");
function getStatusString(s) {
switch(s) {
case BlockchainBackend.NotStarted: return qsTr("Not Started")
case BlockchainBackend.Starting: return qsTr("Starting...")
case BlockchainBackend.Running: return qsTr("Running")
case BlockchainBackend.Stopping: return qsTr("Stopping...")
case BlockchainBackend.Stopped: return qsTr("Stopped")
case BlockchainBackend.Error: return qsTr("Error")
case BlockchainBackend.ErrorNotInitialized: return qsTr("Error: Module not initialized")
case BlockchainBackend.ErrorConfigMissing: return qsTr("Error: Config path missing")
case BlockchainBackend.ErrorStartFailed: return qsTr("Error: Failed to start node")
case BlockchainBackend.ErrorStopFailed: return qsTr("Error: Failed to stop node")
case BlockchainBackend.ErrorSubscribeFailed: return qsTr("Error: Failed to subscribe to events")
default: return qsTr("Unknown")
}
}
function getStatusColor(status) {
switch(status) {
case BlockchainBackend.Running: return Theme.palette.success;
case BlockchainBackend.Starting: return Theme.palette.warning;
case BlockchainBackend.Stopping: return Theme.palette.warning;
case BlockchainBackend.NotStarted: return Theme.palette.error;
case BlockchainBackend.Stopped: return Theme.palette.error;
case BlockchainBackend.Error:
case BlockchainBackend.ErrorNotInitialized:
case BlockchainBackend.ErrorConfigMissing:
case BlockchainBackend.ErrorStartFailed:
case BlockchainBackend.ErrorStopFailed:
case BlockchainBackend.ErrorSubscribeFailed: return Theme.palette.error;
default: return Theme.palette.textSecondary;
function getStatusColor(s) {
switch(s) {
case BlockchainBackend.Running: return Theme.palette.success
case BlockchainBackend.Starting:
case BlockchainBackend.Stopping: return Theme.palette.warning
default: return Theme.palette.error
}
}
property int currentPage: 0 // 0 = config choice (page 1), 1 = node + wallet + logs (page 2)
property int currentPage: 0
}
color: Theme.palette.background
// Loading state before backend connects
ColumnLayout {
anchors.centerIn: parent
visible: !root.ready
spacing: 12
Text {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Connecting to blockchain backend...")
color: Theme.palette.textSecondary
font.pixelSize: Theme.typography.secondaryText
}
BusyIndicator { Layout.alignment: Qt.AlignHCenter; running: !root.ready }
}
StackLayout {
anchors.fill: parent
anchors.margins: Theme.spacing.large
currentIndex: _d.currentPage
visible: root.ready
// Page 1: Config choice (Option 1: Generate own config, Option 2: Set path to configs)
// Page 1: Config choice
ScrollView {
id: configChoiceScrollView
clip: true
ConfigChoiceView {
id: configChoiceView
width: configChoiceScrollView.availableWidth
userConfigPath: backend.userConfig
deploymentConfigPath: backend.deploymentConfig
generatedUserConfigPath: backend.generatedUserConfigPath
onUserConfigPathSelected: function(path) { backend.userConfig = path }
onDeploymentConfigPathSelected: function(path) { backend.deploymentConfig = path }
userConfigPath: root.backend ? root.backend.userConfig : ""
deploymentConfigPath: root.backend ? root.backend.deploymentConfig : ""
generatedUserConfigPath: root.backend ? root.backend.generatedUserConfigPath : ""
onUserConfigPathSelected: function(path) {
if (root.backend) root.backend.userConfig = path
}
onDeploymentConfigPathSelected: function(path) {
if (root.backend) root.backend.deploymentConfig = path
}
onSetPathToConfigsRequested: function() {
backend.useGeneratedConfig = false
if (root.backend) root.backend.useGeneratedConfig = false
_d.currentPage = 1
}
onGenerateRequested: function(outputPath, initialPeers, netPort, blendPort, httpAddr, externalAddress, noPublicIpCheck, deploymentMode, deploymentConfigPath, statePath) {
if (!root.backend) return
console.log("[BlockchainView] generateRequested: outputPath=", outputPath,
"initialPeers=", JSON.stringify(initialPeers),
"netPort=", netPort, "blendPort=", blendPort,
"httpAddr=", httpAddr, "externalAddress=", externalAddress,
"noPublicIpCheck=", noPublicIpCheck, "deploymentMode=", deploymentMode,
"deploymentConfigPath=", deploymentConfigPath, "statePath=", statePath)
configChoiceView.generateResultSuccess = false
configChoiceView.generateResultMessage = ""
var code = backend.generateConfig(outputPath, initialPeers, netPort, blendPort, httpAddr, externalAddress, noPublicIpCheck, deploymentMode, deploymentConfigPath, statePath)
configChoiceView.generateResultSuccess = (code === 0)
configChoiceView.generateResultMessage = code === 0 ? qsTr("Config generated successfully.") : qsTr("Generate failed (code: %1).").arg(code)
if (code === 0) {
backend.userConfig = (outputPath !== "") ? outputPath : backend.generatedUserConfigPath
backend.deploymentConfig = (deploymentMode === 1 && deploymentConfigPath !== "") ? deploymentConfigPath : ""
backend.useGeneratedConfig = true
_d.currentPage = 1
}
logos.watch(
root.backend.generateConfig(
outputPath, initialPeers, netPort, blendPort,
httpAddr, externalAddress, noPublicIpCheck,
deploymentMode, deploymentConfigPath, statePath),
function(code) {
// logos.watch stringifies the returned int coerce back.
var rc = parseInt(code, 10)
console.log("[BlockchainView] generateConfig success callback: code=", code, "type=", typeof code, "→ rc=", rc)
configChoiceView.generateResultSuccess = (rc === 0)
configChoiceView.generateResultMessage =
rc === 0
? qsTr("Config generated successfully.")
: qsTr("Generate failed (code: %1).").arg(rc)
if (rc === 0) {
root.backend.userConfig = (outputPath !== "")
? outputPath : root.backend.generatedUserConfigPath
root.backend.deploymentConfig =
(deploymentMode === 1 && deploymentConfigPath !== "")
? deploymentConfigPath : ""
root.backend.useGeneratedConfig = true
_d.currentPage = 1
}
},
function(error) {
console.log("[BlockchainView] generateConfig error callback: error=", error)
configChoiceView.generateResultSuccess = false
configChoiceView.generateResultMessage =
qsTr("Generate failed: %1").arg(error)
}
)
}
}
}
// Page 2: Start node, balances, transfer, logs
// Page 2: Node control, wallet, logs
SplitView {
orientation: Qt.Vertical
@ -97,39 +166,61 @@ Rectangle {
StatusConfigView {
Layout.fillWidth: true
statusText: _d.getStatusString(backend.status)
statusColor: _d.getStatusColor(backend.status)
userConfig: backend.userConfig
deploymentConfig: backend.deploymentConfig
useGeneratedConfig: backend.useGeneratedConfig
canStart: !!backend.userConfig
&& backend.status !== BlockchainBackend.Starting
&& backend.status !== BlockchainBackend.Stopping
isRunning: backend.status === BlockchainBackend.Running
statusText: root.backend
? _d.getStatusString(root.backend.status)
: qsTr("Not Connected")
statusColor: root.backend
? _d.getStatusColor(root.backend.status)
: Theme.palette.error
userConfig: root.backend ? root.backend.userConfig : ""
deploymentConfig: root.backend ? root.backend.deploymentConfig : ""
useGeneratedConfig: root.backend ? root.backend.useGeneratedConfig : false
canStart: root.backend
&& !!root.backend.userConfig
&& root.backend.status !== BlockchainBackend.Starting
&& root.backend.status !== BlockchainBackend.Stopping
isRunning: root.backend
? root.backend.status === BlockchainBackend.Running
: false
onStartRequested: backend.startBlockchain()
onStopRequested: backend.stopBlockchain()
onStartRequested: if (root.backend) root.backend.startBlockchain()
onStopRequested: if (root.backend) root.backend.stopBlockchain()
onChangeConfigRequested: _d.currentPage = 0
}
WalletView {
id: walletView
accountsModel: backend.accountsModel
accountsModel: root.accountsModel
onGetBalanceRequested: function(addressHex) {
var result = backend.getBalance(addressHex)
if ((result || "").indexOf("Error") === 0) {
lastBalanceErrorAddress = addressHex
lastBalanceError = result
}
else {
lastBalanceErrorAddress = ""
lastBalanceError = ""
}
if (!root.backend) return
logos.watch(
root.backend.getBalance(addressHex),
function(result) {
if ((result || "").indexOf("Error") === 0) {
walletView.lastBalanceErrorAddress = addressHex
walletView.lastBalanceError = result
} else {
walletView.lastBalanceErrorAddress = ""
walletView.lastBalanceError = ""
}
},
function(error) {
walletView.lastBalanceErrorAddress = addressHex
walletView.lastBalanceError = "Error: " + error
}
)
}
onCopyToClipboard: (text) => {
if (root.backend) root.backend.copyToClipboard(text)
}
onCopyToClipboard: (text) => backend.copyToClipboard(text)
onTransferRequested: function(fromKeyHex, toKeyHex, amount) {
walletView.setTransferResult(backend.transferFunds(fromKeyHex, toKeyHex, amount))
if (!root.backend) return
logos.watch(
root.backend.transferFunds(fromKeyHex, toKeyHex, amount),
function(result) { walletView.setTransferResult(result) },
function(error) { walletView.setTransferResult("Error: " + error) }
)
}
}
@ -142,11 +233,12 @@ Rectangle {
SplitView.fillWidth: true
SplitView.minimumHeight: 150
logModel: backend.logModel
onClearRequested: backend.clearLogs()
onCopyToClipboard: (text) => backend.copyToClipboard(text)
logModel: root.logModel
onClearRequested: if (root.backend) root.backend.clearLogs()
onCopyToClipboard: (text) => {
if (root.backend) root.backend.copyToClipboard(text)
}
}
}
}
}

View File

@ -50,7 +50,7 @@ ItemDelegate {
Layout.preferredWidth: 40
display: AbstractButton.IconOnly
flat: true
icon.source: "qrc:/icons/refresh.svg"
icon.source: Qt.resolvedUrl("../icons/refresh.svg")
icon.color: Theme.palette.textSecondary
font.pixelSize: Theme.typography.secondaryText
padding: 4

View File

@ -1,6 +1,8 @@
import QtQuick
import QtQuick.Controls
import Logos.Theme
Button {
id: root
@ -11,7 +13,7 @@ Button {
display: AbstractButton.IconOnly
flat: true
property string iconSource: "qrc:/icons/copy.svg"
property string iconSource: Qt.resolvedUrl("../icons/copy.svg")
icon.source: root.iconSource
icon.width: 24
@ -19,7 +21,7 @@ Button {
icon.color: Theme.palette.textSecondary
function reset() {
iconSource = "qrc:/icons/copy.svg"
iconSource = Qt.resolvedUrl("../icons/copy.svg")
}
Timer {
@ -31,7 +33,7 @@ Button {
onClicked: {
root.copyText()
root.iconSource = "qrc:/icons/checkmark.svg"
root.iconSource = Qt.resolvedUrl("../icons/checkmark.svg")
resetTimer.restart()
}
}

View File

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 462 B

View File

Before

Width:  |  Height:  |  Size: 976 B

After

Width:  |  Height:  |  Size: 976 B

View File

Before

Width:  |  Height:  |  Size: 634 B

After

Width:  |  Height:  |  Size: 634 B

View File

@ -85,17 +85,13 @@ ColumnLayout {
resultSuccess: root.generateResultSuccess
resultMessage: root.generateResultMessage
Layout.fillWidth: true
onGenerateRequested: root.generateRequested(
outputPath,
initialPeers,
netPort,
blendPort,
httpAddr,
externalAddress,
noPublicIpCheck,
deploymentMode,
deploymentConfigPath,
statePath)
onGenerateRequested: function(outputPath, initialPeers, netPort, blendPort,
httpAddr, externalAddress, noPublicIpCheck,
deploymentMode, deploymentConfigPath, statePath) {
root.generateRequested(outputPath, initialPeers, netPort, blendPort,
httpAddr, externalAddress, noPublicIpCheck,
deploymentMode, deploymentConfigPath, statePath)
}
}
}
}

View File

@ -210,7 +210,7 @@ ColumnLayout {
FileDialog {
id: deploymentConfigFileDialog
modality: Qt.NonModal
nameFilters: ["YAML files (*.yaml)", "All files (*)"]
nameFilters: ["YAML files (*.yaml *.yml)", "All files (*)"]
currentFolder: StandardPaths.standardLocations(StandardPaths.DocumentsLocation)[0]
onAccepted: customDeploymentField.text = selectedFile
}

View File

@ -9,7 +9,7 @@ Control {
id: root
// --- Public API ---
required property var logModel // LogModel (QAbstractListModel with "text" role)
required property var logModel // ListModel with "text" role
signal clearRequested()
signal copyToClipboard(string text)
@ -61,6 +61,12 @@ Control {
model: root.logModel
spacing: 2
// Auto-scroll to the latest log on insert. Use the ListView's
// own `count` (it's always available and emits countChanged)
// the model replica is a QAbstractItemModelReplica and does
// not carry the source-side `count` Q_PROPERTY through QtRO.
onCountChanged: if (count > 0) positionViewAtEnd()
delegate: ItemDelegate{
width: ListView.view.width
contentItem: LogosText {
@ -76,20 +82,15 @@ Control {
}
LogosText {
visible: !root.logModel || root.logModel.count === 0
// ListView's `count` reflects the model row count and has
// a NOTIFY signal using it here gives the binding
// automatic refresh, unlike `root.logModel.count`.
visible: logsListView.count === 0
anchors.centerIn: parent
text: qsTr("No logs yet...")
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
}
Connections {
target: root.logModel
function onCountChanged() {
if (root.logModel.count > 0)
logsListView.positionViewAtEnd()
}
}
}
}
}

View File

@ -78,14 +78,14 @@ ColumnLayout {
FileDialog {
id: userConfigFileDialog
modality: Qt.NonModal
nameFilters: ["YAML files (*.yaml)"]
nameFilters: ["YAML files (*.yaml *.yml)", "All files (*)"]
onAccepted: root.userConfigPathSelected(selectedFile)
}
FileDialog {
id: deploymentConfigFileDialog
modality: Qt.NonModal
nameFilters: ["YAML files (*.yaml)"]
nameFilters: ["YAML files (*.yaml *.yml)", "All files (*)"]
onAccepted: root.deploymentConfigPathSelected(selectedFile)
}
}

35
tests/ui-tests.mjs Normal file
View File

@ -0,0 +1,35 @@
import { resolve } from "node:path";
// CI sets LOGOS_QT_MCP automatically; for interactive use:
// nix build .#test-framework -o result-mcp
const root =
process.env.LOGOS_QT_MCP ||
new URL("../result-mcp", import.meta.url).pathname;
const { test, run } = await import(resolve(root, "test-framework/framework.mjs"));
// Smoke test: the blockchain UI module must load in the host
// (logos-standalone-app / logos-basecamp), connect to its process-isolated
// C++ backend over Qt Remote Objects, and render the QML view — even when the
// backend node module is not running yet (the node is started from this UI).
test("blockchain_ui: backend connects and config view renders", async (app) => {
await app.waitFor(
async () => {
// Once the BlockchainBackend replica is Valid, the loading state is
// replaced by the ConfigChoiceView. This static label proves the QML
// (including the Logos.Theme / Logos.Controls design-system imports)
// loaded and the backend replica connected.
await app.expectTexts(["Choose how to set up your node config"]);
},
{
timeout: 30000,
interval: 1000,
description: "blockchain UI to load and backend to connect",
}
);
});
test("blockchain_ui: config setup actions are visible", async (app) => {
await app.expectTexts(["Generate config", "Set path to config"]);
});
run();