feat(commands): Expose commands to module (#33)

* Expose commands to module

* Add missing tests stubs
This commit is contained in:
Daniel Sanchez 2026-06-15 13:17:25 +02:00 committed by GitHub
parent f3558372fd
commit bcd885c09f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 552 additions and 8 deletions

15
flake.lock generated
View File

@ -24,16 +24,16 @@
"rust-rapidsnark": "rust-rapidsnark"
},
"locked": {
"lastModified": 1781108021,
"narHash": "sha256-YOJR3TMNHgQedoN7ftga+EP3VzWkCVMW59xYTAMF/pU=",
"lastModified": 1781288172,
"narHash": "sha256-4anGBb7slL6WfMCGlI4BmLAjim7kQOMD/qv4FCPJwII=",
"owner": "logos-blockchain",
"repo": "logos-blockchain",
"rev": "feddb4616512bf7954b503d8de4287d2ed59c4ba",
"rev": "86e24cfd985b6341e96bba76ecad36ff379bd243",
"type": "github"
},
"original": {
"owner": "logos-blockchain",
"ref": "feddb4616512bf7954b503d8de4287d2ed59c4ba",
"ref": "86e24cfd985b6341e96bba76ecad36ff379bd243",
"repo": "logos-blockchain",
"type": "github"
}
@ -53,6 +53,7 @@
"original": {
"owner": "logos-blockchain",
"repo": "logos-blockchain-circuits",
"rev": "2846ee7a4cfa24458bb8063412ab2e753b344d2f",
"type": "github"
}
},
@ -6313,11 +6314,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1780749050,
"narHash": "sha256-3av0pIjlOWQ6rDbNOmpUSvbNnJkGORQKKjb4LtCZsIY=",
"lastModified": 1781074563,
"narHash": "sha256-md8WlXOlfnIeHeOScMTTHFyf2d6iaTwPl2apR5EQ3P4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a799d3e3886da994fa307f817a6bc705ae538eeb",
"rev": "9ae611a455b90cf061d8f332b977e387bda8e1ca",
"type": "github"
},
"original": {

View File

@ -3,7 +3,8 @@
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder";
logos-blockchain.url = "github:logos-blockchain/logos-blockchain?ref=feddb4616512bf7954b503d8de4287d2ed59c4ba"; # v0.1.3-rc.10-compatible + rust-rapidsnark nix fixes
# v0.1.3-rc.10-compatible + rust-rapidsnark nix fixes + cli commands
logos-blockchain.url = "github:logos-blockchain/logos-blockchain?ref=86e24cfd985b6341e96bba76ecad36ff379bd243";
};
outputs = inputs@{ logos-module-builder, ... }:

View File

@ -1,7 +1,9 @@
#include "logos_blockchain_module.h"
#include <algorithm>
#include <boost/algorithm/hex.hpp>
#include <boost/algorithm/string/trim.hpp>
#include <cctype>
#include <charconv>
#include <cstdio>
#include <cstdlib>
@ -52,6 +54,23 @@ namespace {
return out;
}
// Map a "ed25519" / "zk" string (case-insensitive) to the C KeyType enum.
bool parse_key_type(const std::string& s, KeyType& out) {
std::string lower = s;
boost::algorithm::trim(lower);
std::transform(lower.begin(), lower.end(), lower.begin(),
[](unsigned char c) { return std::tolower(c); });
if (lower == "ed25519") {
out = KeyType::Ed25519;
return true;
}
if (lower == "zk") {
out = KeyType::Zk;
return true;
}
return false;
}
// Wrapper that owns data and provides GenerateConfigArgs
struct OwnedGenerateConfigArgs {
std::vector<std::string> initial_peers_data;
@ -323,6 +342,162 @@ int LogosBlockchainModule::stop() {
return 0;
}
// Config management
int LogosBlockchainModule::update_user_config(const std::string& user_config_path, const std::string& keystore_path) {
const std::string config = localPathFromFileUrl(user_config_path);
const std::string keystore = localPathFromFileUrl(keystore_path);
const OperationStatus status = ::update_user_config(config.c_str(), keystore.c_str());
if (!is_ok(&status)) {
fprintf(stderr, "Failed to update user config. Error: %d\n", status);
return 1;
}
return 0;
}
int LogosBlockchainModule::migrate_user_config(const std::string& output_path, const std::string& keystore_path) {
const std::string output = localPathFromFileUrl(output_path);
const std::string keystore = localPathFromFileUrl(keystore_path);
const OperationStatus status = ::migrate_user_config(output.c_str(), keystore.c_str());
if (!is_ok(&status)) {
fprintf(stderr, "Failed to migrate user config. Error: %d\n", status);
return 1;
}
return 0;
}
int LogosBlockchainModule::migrate_user_config_0_1_2(
const std::string& new_config_path,
const std::string& old_config_path,
const std::string& keystore_path
) {
const std::string new_config = localPathFromFileUrl(new_config_path);
const std::string old_config = localPathFromFileUrl(old_config_path);
const std::string keystore = localPathFromFileUrl(keystore_path);
const OperationStatus status =
::migrate_user_config_0_1_2(new_config.c_str(), old_config.c_str(), keystore.c_str());
if (!is_ok(&status)) {
fprintf(stderr, "Failed to migrate 0.1.2 config. Error: %d\n", status);
return 1;
}
return 0;
}
int LogosBlockchainModule::participate(
const std::string& config_path,
const std::string& keystore_path,
const std::string& output_dir,
const std::string& external_address
) {
const std::string config = localPathFromFileUrl(config_path);
const std::string keystore = localPathFromFileUrl(keystore_path);
const std::string output = localPathFromFileUrl(output_dir);
const char* external_address_ptr = external_address.empty() ? nullptr : external_address.c_str();
const OperationStatus status =
::participate(config.c_str(), keystore.c_str(), output.c_str(), external_address_ptr);
if (!is_ok(&status)) {
fprintf(stderr, "Failed to generate participation data. Error: %d\n", status);
return 1;
}
return 0;
}
// Keystore
std::string LogosBlockchainModule::generate_key(
const std::string& user_config_path,
const std::string& keystore_path,
const std::string& key_type,
const std::string& key_title
) {
KeyType type{};
if (!parse_key_type(key_type, type)) {
return "Error: Invalid key_type (expected \"ed25519\" or \"zk\").";
}
const std::string config = localPathFromFileUrl(user_config_path);
const std::string keystore = localPathFromFileUrl(keystore_path);
const char* key_title_ptr = key_title.empty() ? nullptr : key_title.c_str();
auto [value, error] = ::generate_key(config.c_str(), keystore.c_str(), type, key_title_ptr);
if (!is_ok(&error)) {
fprintf(stderr, "Failed to generate key. Error: %d\n", error);
return "Error: Failed to generate key: " + std::to_string(error);
}
std::string result(value);
const OperationStatus free_status = free_cstring(value);
if (!is_ok(&free_status)) {
fprintf(stderr, "Failed to free key id string. Error: %d\n", free_status);
}
return result;
}
int LogosBlockchainModule::add_key(
const std::string& user_config_path,
const std::string& keystore_path,
const std::string& key_type,
const std::string& key_hex,
const std::string& key_title
) {
KeyType type{};
if (!parse_key_type(key_type, type)) {
fprintf(stderr, "Invalid key_type (expected \"ed25519\" or \"zk\").\n");
return 1;
}
const std::string config = localPathFromFileUrl(user_config_path);
const std::string keystore = localPathFromFileUrl(keystore_path);
const char* key_title_ptr = key_title.empty() ? nullptr : key_title.c_str();
const OperationStatus status =
::add_key(config.c_str(), keystore.c_str(), type, key_hex.c_str(), key_title_ptr);
if (!is_ok(&status)) {
fprintf(stderr, "Failed to add key. Error: %d\n", status);
return 1;
}
return 0;
}
int LogosBlockchainModule::remove_key(
const std::string& user_config_path,
const std::string& keystore_path,
const std::string& key_title
) {
const std::string config = localPathFromFileUrl(user_config_path);
const std::string keystore = localPathFromFileUrl(keystore_path);
const OperationStatus status = ::remove_key(config.c_str(), keystore.c_str(), key_title.c_str());
if (!is_ok(&status)) {
fprintf(stderr, "Failed to remove key. Error: %d\n", status);
return 1;
}
return 0;
}
// Identity
std::string LogosBlockchainModule::get_peer_id(const std::string& config_path) {
const std::string config = localPathFromFileUrl(config_path);
auto [value, error] = ::get_peer_id(config.c_str());
if (!is_ok(&error)) {
fprintf(stderr, "Failed to get peer id. Error: %d\n", error);
return "Error: Failed to get peer id: " + std::to_string(error);
}
std::string result(value);
const OperationStatus free_status = free_cstring(value);
if (!is_ok(&free_status)) {
fprintf(stderr, "Failed to free peer id string. Error: %d\n", free_status);
}
return result;
}
// Wallet
std::string LogosBlockchainModule::wallet_get_balance(const std::string& address_hex) {

View File

@ -26,6 +26,45 @@ public:
int start(const std::string& config_path, const std::string& deployment);
int stop();
// Config management
int update_user_config(const std::string& user_config_path, const std::string& keystore_path);
int migrate_user_config(const std::string& output_path, const std::string& keystore_path);
int migrate_user_config_0_1_2(
const std::string& new_config_path,
const std::string& old_config_path,
const std::string& keystore_path
);
int participate(
const std::string& config_path,
const std::string& keystore_path,
const std::string& output_dir,
const std::string& external_address
);
// Keystore
// key_type is "ed25519" or "zk". key_title may be empty (auto-generated).
std::string generate_key(
const std::string& user_config_path,
const std::string& keystore_path,
const std::string& key_type,
const std::string& key_title
);
int add_key(
const std::string& user_config_path,
const std::string& keystore_path,
const std::string& key_type,
const std::string& key_hex,
const std::string& key_title
);
int remove_key(
const std::string& user_config_path,
const std::string& keystore_path,
const std::string& key_title
);
// Identity
std::string get_peer_id(const std::string& config_path);
// Wallet
std::string wallet_get_balance(const std::string& address_hex);
std::string wallet_transfer_funds(

View File

@ -12,6 +12,7 @@ logos_test(
TEST_SOURCES
main.cpp
test_blockchain.cpp
event_stubs.cpp
MOCK_C_SOURCES
mocks/mock_logos_blockchain.cpp
EXTRA_INCLUDES
@ -35,6 +36,7 @@ if(LIBLOGOS_BLOCKCHAIN_PATH)
TEST_SOURCES
main.cpp
test_blockchain.cpp
event_stubs.cpp
EXTRA_INCLUDES
../lib
EXTRA_LINK_LIBS

17
tests/event_stubs.cpp Normal file
View File

@ -0,0 +1,17 @@
// Test-only definitions for `logos_events:` methods.
//
// In a real build the cpp-generator emits the bodies of every `logos_events:`
// method into a sidecar `<name>_events.cpp` (each forwards through
// `emitEventImpl_`). The unit/integration test targets compile the module
// source directly without running codegen, so those bodies are absent and the
// link fails on `on_new_block_callback`'s reference to `newBlock`.
//
// `emitEventImpl_` is a no-op outside a framework-provisioned context (no emit
// callback is installed in tests), so forwarding here mirrors production
// behaviour while keeping the test build link-complete.
#include "logos_blockchain_module.h"
void LogosBlockchainModule::newBlock(const std::string& blockJson) {
emitEventImpl_("newBlock", nullptr);
}

View File

@ -42,6 +42,78 @@ OperationStatus stop_node(LogosBlockchainNode* node) {
return 0;
}
OperationStatus update_user_config(const char* user_config_path, const char* keystore_path) {
LOGOS_CMOCK_RECORD("update_user_config");
return LOGOS_CMOCK_RETURN(int, "update_user_config");
}
OperationStatus migrate_user_config(const char* output_path, const char* keystore_path) {
LOGOS_CMOCK_RECORD("migrate_user_config");
return LOGOS_CMOCK_RETURN(int, "migrate_user_config");
}
OperationStatus migrate_user_config_0_1_2(
const char* new_config_path,
const char* old_config_path,
const char* keystore_path)
{
LOGOS_CMOCK_RECORD("migrate_user_config_0_1_2");
return LOGOS_CMOCK_RETURN(int, "migrate_user_config_0_1_2");
}
OperationStatus participate(
const char* config_path,
const char* keystore_path,
const char* output_dir,
const char* external_address)
{
LOGOS_CMOCK_RECORD("participate");
return LOGOS_CMOCK_RETURN(int, "participate");
}
StringResult generate_key(
const char* user_config_path,
const char* keystore_path,
KeyType key_type,
const char* key_title)
{
LOGOS_CMOCK_RECORD("generate_key");
StringResult result;
const char* id = LOGOS_CMOCK_RETURN_STRING("generate_key");
result.value = id ? strdup(id) : nullptr;
result.error = LOGOS_CMOCK_RETURN(int, "generate_key_error");
return result;
}
OperationStatus add_key(
const char* user_config_path,
const char* keystore_path,
KeyType key_type,
const char* key_hex,
const char* key_title)
{
LOGOS_CMOCK_RECORD("add_key");
return LOGOS_CMOCK_RETURN(int, "add_key");
}
OperationStatus remove_key(
const char* user_config_path,
const char* keystore_path,
const char* key_title)
{
LOGOS_CMOCK_RECORD("remove_key");
return LOGOS_CMOCK_RETURN(int, "remove_key");
}
StringResult get_peer_id(const char* config_path) {
LOGOS_CMOCK_RECORD("get_peer_id");
StringResult result;
const char* id = LOGOS_CMOCK_RETURN_STRING("get_peer_id");
result.value = id ? strdup(id) : nullptr;
result.error = LOGOS_CMOCK_RETURN(int, "get_peer_id_error");
return result;
}
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");

View File

@ -30,6 +30,9 @@ typedef enum { Devnet } WellKnownDeployment;
// Consensus state enum
typedef enum { Bootstrapping, Online } State;
// Key type for generate_key / add_key
typedef enum { Ed25519, Zk } KeyType;
// Deployment configuration
typedef struct {
DeploymentType deployment_type;
@ -97,6 +100,39 @@ 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);
// Config management
OperationStatus update_user_config(const char* user_config_path, const char* keystore_path);
OperationStatus migrate_user_config(const char* output_path, const char* keystore_path);
OperationStatus migrate_user_config_0_1_2(
const char* new_config_path,
const char* old_config_path,
const char* keystore_path);
OperationStatus participate(
const char* config_path,
const char* keystore_path,
const char* output_dir,
const char* external_address);
// Keystore
StringResult generate_key(
const char* user_config_path,
const char* keystore_path,
KeyType key_type,
const char* key_title);
OperationStatus add_key(
const char* user_config_path,
const char* keystore_path,
KeyType key_type,
const char* key_hex,
const char* key_title);
OperationStatus remove_key(
const char* user_config_path,
const char* keystore_path,
const char* key_title);
// Identity
StringResult get_peer_id(const char* config_path);
// Wallet
BalanceResult get_balance(LogosBlockchainNode* node, const uint8_t* address, const void* reserved);
TransferHashResult transfer_funds(LogosBlockchainNode* node, const TransferFundsArguments* args);

View File

@ -727,3 +727,204 @@ LOGOS_TEST(get_cryptarchia_info_returns_error_on_ffi_failure) {
LOGOS_ASSERT_TRUE(starts_with(module->get_cryptarchia_info(), "Error:"));
delete module;
}
// ============================================================================
// Config management (operate on file paths, no running node required)
// ============================================================================
LOGOS_TEST(update_user_config_returns_0_on_success) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
t.mockCFunction("update_user_config").returns(0);
LOGOS_ASSERT_EQ(module.update_user_config("/tmp/config.yaml", "/tmp/keystore.yaml"), 0);
LOGOS_ASSERT(t.cFunctionCalled("update_user_config"));
}
LOGOS_TEST(update_user_config_returns_1_on_failure) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
t.mockCFunction("update_user_config").returns(1);
LOGOS_ASSERT_EQ(module.update_user_config("/tmp/config.yaml", "/tmp/keystore.yaml"), 1);
}
LOGOS_TEST(migrate_user_config_returns_0_on_success) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
t.mockCFunction("migrate_user_config").returns(0);
LOGOS_ASSERT_EQ(module.migrate_user_config("/tmp/out.yaml", "/tmp/keystore.yaml"), 0);
LOGOS_ASSERT(t.cFunctionCalled("migrate_user_config"));
}
LOGOS_TEST(migrate_user_config_returns_1_on_failure) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
t.mockCFunction("migrate_user_config").returns(1);
LOGOS_ASSERT_EQ(module.migrate_user_config("/tmp/out.yaml", "/tmp/keystore.yaml"), 1);
}
LOGOS_TEST(migrate_user_config_0_1_2_returns_0_on_success) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
t.mockCFunction("migrate_user_config_0_1_2").returns(0);
LOGOS_ASSERT_EQ(module.migrate_user_config_0_1_2("/tmp/new.yaml", "/tmp/old.yaml", "/tmp/keystore.yaml"), 0);
LOGOS_ASSERT(t.cFunctionCalled("migrate_user_config_0_1_2"));
}
LOGOS_TEST(migrate_user_config_0_1_2_returns_1_on_failure) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
t.mockCFunction("migrate_user_config_0_1_2").returns(1);
LOGOS_ASSERT_EQ(module.migrate_user_config_0_1_2("/tmp/new.yaml", "/tmp/old.yaml", "/tmp/keystore.yaml"), 1);
}
LOGOS_TEST(participate_returns_0_on_success) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
t.mockCFunction("participate").returns(0);
LOGOS_ASSERT_EQ(module.participate("/tmp/config.yaml", "/tmp/keystore.yaml", "/tmp/out", ""), 0);
LOGOS_ASSERT(t.cFunctionCalled("participate"));
}
LOGOS_TEST(participate_returns_1_on_failure) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
t.mockCFunction("participate").returns(1);
LOGOS_ASSERT_EQ(module.participate("/tmp/config.yaml", "/tmp/keystore.yaml", "/tmp/out", "1.2.3.4"), 1);
}
// ============================================================================
// Keystore (generate_key / add_key / remove_key)
// ============================================================================
LOGOS_TEST(generate_key_returns_id_on_success) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
t.mockCFunction("generate_key").returns("key-abc123");
t.mockCFunction("generate_key_error").returns(0);
std::string result = module.generate_key("/tmp/config.yaml", "/tmp/keystore.yaml", "ed25519", "");
LOGOS_ASSERT_EQ(result, std::string("key-abc123"));
LOGOS_ASSERT(t.cFunctionCalled("generate_key"));
LOGOS_ASSERT(t.cFunctionCalled("free_cstring"));
}
LOGOS_TEST(generate_key_accepts_zk_type) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
t.mockCFunction("generate_key").returns("zk-key");
t.mockCFunction("generate_key_error").returns(0);
std::string result = module.generate_key("/tmp/config.yaml", "/tmp/keystore.yaml", "ZK", "my-title");
LOGOS_ASSERT_EQ(result, std::string("zk-key"));
}
LOGOS_TEST(generate_key_rejects_invalid_key_type) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
std::string result = module.generate_key("/tmp/config.yaml", "/tmp/keystore.yaml", "rsa", "");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
LOGOS_ASSERT_TRUE(contains(result, "key_type"));
LOGOS_ASSERT_FALSE(t.cFunctionCalled("generate_key"));
}
LOGOS_TEST(generate_key_returns_error_on_ffi_failure) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
t.mockCFunction("generate_key_error").returns(1);
std::string result = module.generate_key("/tmp/config.yaml", "/tmp/keystore.yaml", "ed25519", "");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
}
LOGOS_TEST(add_key_returns_0_on_success) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
t.mockCFunction("add_key").returns(0);
LOGOS_ASSERT_EQ(module.add_key("/tmp/config.yaml", "/tmp/keystore.yaml", "ed25519", VALID_HEX, ""), 0);
LOGOS_ASSERT(t.cFunctionCalled("add_key"));
}
LOGOS_TEST(add_key_rejects_invalid_key_type) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
LOGOS_ASSERT_EQ(module.add_key("/tmp/config.yaml", "/tmp/keystore.yaml", "bogus", VALID_HEX, ""), 1);
LOGOS_ASSERT_FALSE(t.cFunctionCalled("add_key"));
}
LOGOS_TEST(add_key_returns_1_on_failure) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
t.mockCFunction("add_key").returns(1);
LOGOS_ASSERT_EQ(module.add_key("/tmp/config.yaml", "/tmp/keystore.yaml", "zk", VALID_HEX, "title"), 1);
}
LOGOS_TEST(remove_key_returns_0_on_success) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
t.mockCFunction("remove_key").returns(0);
LOGOS_ASSERT_EQ(module.remove_key("/tmp/config.yaml", "/tmp/keystore.yaml", "my-key"), 0);
LOGOS_ASSERT(t.cFunctionCalled("remove_key"));
}
LOGOS_TEST(remove_key_returns_1_on_failure) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
t.mockCFunction("remove_key").returns(1);
LOGOS_ASSERT_EQ(module.remove_key("/tmp/config.yaml", "/tmp/keystore.yaml", "my-key"), 1);
}
// ============================================================================
// Identity (get_peer_id)
// ============================================================================
LOGOS_TEST(get_peer_id_returns_id_on_success) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
t.mockCFunction("get_peer_id").returns("12D3KooWPeerId");
t.mockCFunction("get_peer_id_error").returns(0);
std::string result = module.get_peer_id("/tmp/config.yaml");
LOGOS_ASSERT_EQ(result, std::string("12D3KooWPeerId"));
LOGOS_ASSERT(t.cFunctionCalled("get_peer_id"));
LOGOS_ASSERT(t.cFunctionCalled("free_cstring"));
}
LOGOS_TEST(get_peer_id_returns_error_on_ffi_failure) {
auto t = LogosTestContext("blockchain_module");
LogosBlockchainModule module;
t.mockCFunction("get_peer_id_error").returns(1);
std::string result = module.get_peer_id("/tmp/config.yaml");
LOGOS_ASSERT_TRUE(starts_with(result, "Error:"));
}