logos-blockchain-module/src/logos_blockchain_module.cpp
Andrus Salumets c9829f2fa6
feat(wallet): expose claimable vouchers (#37)
Co-authored-by: Daniel <sanchez.quiros.daniel@gmail.com>
2026-06-18 16:10:34 +02:00

1025 lines
38 KiB
C++

#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>
#include <filesystem>
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
namespace fs = std::filesystem;
using json = nlohmann::json;
// Define static member
LogosBlockchainModule* LogosBlockchainModule::s_instance = nullptr;
namespace {
// Rust `File::open` / `deserialize_config_at_path` only accept real filesystem paths. QML often
// passes `file:///...` URLs; strip to a local path when applicable.
std::string localPathFromFileUrl(const std::string& s) {
if (s.size() >= 7 && s.substr(0, 7) == "file://") return s.substr(7);
if (s.size() >= 5 && s.substr(0, 5) == "file:") return s.substr(5);
return s;
}
// Use the C API type Hash (from logos_blockchain.h) to define address/hash byte size.
constexpr int ADDRESS_BYTES = sizeof(Hash);
constexpr int TX_HASH_BYTES = sizeof(TxHash);
constexpr int ADDRESS_HEX_LEN = ADDRESS_BYTES * 2;
std::vector<uint8_t> parse_address_hex(const std::string& address_hex) {
std::string hex = address_hex;
boost::algorithm::trim(hex);
if (hex.size() >= 2 && hex[0] == '0' && (hex[1] == 'x' || hex[1] == 'X'))
hex = hex.substr(2);
if (static_cast<int>(hex.size()) != ADDRESS_HEX_LEN)
return {};
try {
std::string decoded;
boost::algorithm::unhex(hex.begin(), hex.end(), std::back_inserter(decoded));
return {decoded.begin(), decoded.end()};
} catch (const boost::algorithm::non_hex_input&) {
return {};
}
}
// Parse arbitrary-length hex (optional 0x prefix) into bytes. Unlike
// parse_address_hex this does not enforce a fixed length; used for the
// variable-length channel deposit metadata. Returns false on odd length or
// non-hex input.
bool parse_hex_bytes(const std::string& hex_in, std::vector<uint8_t>& out) {
std::string hex = hex_in;
boost::algorithm::trim(hex);
if (hex.size() >= 2 && hex[0] == '0' && (hex[1] == 'x' || hex[1] == 'X'))
hex = hex.substr(2);
if (hex.size() % 2 != 0)
return false;
try {
std::string decoded;
boost::algorithm::unhex(hex.begin(), hex.end(), std::back_inserter(decoded));
out.assign(decoded.begin(), decoded.end());
return true;
} catch (const boost::algorithm::non_hex_input&) {
return false;
}
}
std::string bytes_to_hex(const uint8_t* data, size_t len) {
std::string out;
out.reserve(len * 2);
boost::algorithm::hex_lower(data, data + len, std::back_inserter(out));
return out;
}
// 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;
std::vector<const char*> initial_peers_ptrs;
uint32_t initial_peers_count_val;
std::string output_data;
uint16_t net_port_val;
uint16_t blend_port_val;
std::string http_addr_data;
std::string external_address_data;
std::string state_path_data;
bool ibd_val;
std::string log_filter_data;
std::string kms_file_data;
// The FFI struct with pointers into owned data
GenerateConfigArgs ffi_args{};
// Constructor that populates both owned data and FFI struct from JSON
explicit OwnedGenerateConfigArgs(const json& args) {
// initial_peers (JSON array -> const char**)
if (args.contains("initial_peers") && args["initial_peers"].is_array()) {
for (const auto& peer : args["initial_peers"]) {
initial_peers_data.push_back(peer.get<std::string>());
}
initial_peers_count_val = static_cast<uint32_t>(initial_peers_data.size());
for (const std::string& data : initial_peers_data) {
initial_peers_ptrs.push_back(data.c_str());
}
ffi_args.initial_peers = initial_peers_ptrs.data();
ffi_args.initial_peers_count = &initial_peers_count_val;
} else {
ffi_args.initial_peers = nullptr;
ffi_args.initial_peers_count = nullptr;
}
// output (string -> const char*)
if (args.contains("output") && args["output"].is_string()) {
output_data = args["output"].get<std::string>();
ffi_args.output = output_data.c_str();
} else {
ffi_args.output = nullptr;
}
// net_port (int -> const uint16_t*)
if (args.contains("net_port") && args["net_port"].is_number_integer()) {
net_port_val = static_cast<uint16_t>(args["net_port"].get<int>());
ffi_args.net_port = &net_port_val;
} else {
ffi_args.net_port = nullptr;
}
// blend_port (int -> const uint16_t*)
if (args.contains("blend_port") && args["blend_port"].is_number_integer()) {
blend_port_val = static_cast<uint16_t>(args["blend_port"].get<int>());
ffi_args.blend_port = &blend_port_val;
} else {
ffi_args.blend_port = nullptr;
}
// http_addr (string -> const char*)
if (args.contains("http_addr") && args["http_addr"].is_string()) {
http_addr_data = args["http_addr"].get<std::string>();
ffi_args.http_addr = http_addr_data.c_str();
} else {
ffi_args.http_addr = nullptr;
}
// external_address (string -> const char*)
if (args.contains("external_address") && args["external_address"].is_string()) {
external_address_data = args["external_address"].get<std::string>();
ffi_args.external_address = external_address_data.c_str();
} else {
ffi_args.external_address = nullptr;
}
// state_path (string -> const char*)
if (args.contains("state_path") && args["state_path"].is_string()) {
state_path_data = args["state_path"].get<std::string>();
ffi_args.state_path = state_path_data.c_str();
} else {
ffi_args.state_path = nullptr;
}
// ibd (bool -> const bool*)
if (args.contains("ibd") && args["ibd"].is_boolean()) {
ibd_val = args["ibd"].get<bool>();
ffi_args.ibd = &ibd_val;
} else {
ffi_args.ibd = nullptr;
}
// log_filter (string -> const char*)
if (args.contains("log_filter") && args["log_filter"].is_string()) {
log_filter_data = args["log_filter"].get<std::string>();
ffi_args.log_filter = log_filter_data.c_str();
} else {
ffi_args.log_filter = nullptr;
}
// kms_file (string -> const char*)
if (args.contains("kms_file") && args["kms_file"].is_string()) {
kms_file_data = args["kms_file"].get<std::string>();
ffi_args.kms_file = kms_file_data.c_str();
} else {
ffi_args.kms_file = nullptr;
}
}
};
} // namespace
namespace environment {
constexpr auto LOGOS_BLOCKCHAIN_CIRCUITS = "LOGOS_BLOCKCHAIN_CIRCUITS";
bool is_circuits_path_valid(const std::string& path) {
std::error_code ec;
if (!fs::is_directory(path, ec)) return false;
for (const auto& entry : fs::directory_iterator(path, ec)) {
if (entry.is_regular_file()) return true;
}
return false;
}
void setup_circuits_path(const std::string& module_path) {
fs::path circuits_path = fs::path(module_path) / "circuits";
std::string circuits_str = circuits_path.string();
if (!is_circuits_path_valid(circuits_str)) {
fprintf(stderr, "FATAL: The LOGOS_BLOCKCHAIN_CIRCUITS environment variable is not set or does not contain any files.\n");
return;
}
setenv("LOGOS_BLOCKCHAIN_CIRCUITS", circuits_str.c_str(), 1);
fprintf(stderr, "LOGOS_BLOCKCHAIN_CIRCUITS set to: %s\n", circuits_str.c_str());
}
} // namespace environment
void LogosBlockchainModule::on_new_block_callback(const char* block) {
if (s_instance) {
fprintf(stderr, "Received new block: %s\n", block);
json j;
j["block"] = std::string(block);
s_instance->newBlock(j.dump());
// SAFETY:
// We are getting an owned pointer here which is freed after this callback is called, so there is no need to
// free the resource here as we are copying the data!
}
}
LogosBlockchainModule::LogosBlockchainModule() {
node = nullptr;
}
LogosBlockchainModule::~LogosBlockchainModule() {
s_instance = nullptr;
if (node) {
stop();
}
}
// ---- Node ----
// Lifecycle
std::string LogosBlockchainModule::generate_user_config(const std::string& json_args) {
json parsed_args;
try {
parsed_args = json::parse(json_args);
} catch (const json::parse_error& e) {
fprintf(stderr, "Failed to parse JSON args: %s\n", e.what());
return "1";
}
const OwnedGenerateConfigArgs owned_args(parsed_args);
const OperationStatus status = ::generate_user_config(owned_args.ffi_args);
if (!is_ok(&status)) {
fprintf(stderr, "Failed to generate user config. Error: %d\n", status);
return "1";
}
return "0";
}
std::string LogosBlockchainModule::start(const std::string& config_path, const std::string& deployment) {
if (node) {
fprintf(stderr, "Could not execute the operation: The node is already running.\n");
return "1";
}
const char* module_path_env = std::getenv("LOGOS_MODULE_PATH");
if (module_path_env && *module_path_env) {
environment::setup_circuits_path(module_path_env);
} else {
fprintf(stderr, "Warning: LOGOS_MODULE_PATH not set, skipping circuits path setup.\n");
}
std::string effective_config_path = config_path;
if (effective_config_path.empty()) {
const char* env = std::getenv("LB_CONFIG_PATH");
if (env && *env) {
effective_config_path = env;
fprintf(stderr, "Using config from LB_CONFIG_PATH: %s\n", effective_config_path.c_str());
} else {
fprintf(stderr, "Config path was not specified and LB_CONFIG_PATH is not set.\n");
return "3";
}
}
effective_config_path = localPathFromFileUrl(effective_config_path);
const std::string deployment_path = localPathFromFileUrl(deployment);
const char* config_path_ptr = effective_config_path.empty() ? nullptr : effective_config_path.c_str();
const char* deployment_ptr = deployment_path.empty() ? nullptr : deployment_path.c_str();
auto [value, error] = start_lb_node(config_path_ptr, deployment_ptr);
fprintf(stderr, "Start node returned with value and error.\n");
if (!is_ok(&error)) {
fprintf(stderr, "Failed to start the node. Error: %d\n", error);
return "4";
}
node = value;
fprintf(stderr, "The node was started successfully.\n");
if (!node) {
fprintf(stderr, "Could not subscribe to block events: The node is not running.\n");
return "4";
}
s_instance = this;
const OperationStatus subscribe_status = subscribe_to_new_blocks(node, on_new_block_callback);
if (!is_ok(&subscribe_status)) {
fprintf(stderr, "Failed to subscribe to new blocks. Error: %d\n", subscribe_status);
return "5";
}
return "0";
}
std::string LogosBlockchainModule::stop() {
if (!node) {
fprintf(stderr, "Could not execute the operation: The node is not running.\n");
return "1";
}
s_instance = nullptr;
const OperationStatus status = stop_node(node);
if (is_ok(&status)) {
fprintf(stderr, "The node was stopped successfully.\n");
} else {
fprintf(stderr, "Could not stop the node. Error: %d\n", status);
}
node = nullptr;
return "0";
}
// Config management
std::string 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";
}
std::string 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";
}
std::string 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";
}
std::string 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;
}
std::string 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";
}
std::string 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) {
fprintf(stderr, "wallet_get_balance: address_hex=%s\n", address_hex.c_str());
if (!node) {
return "Error: The node is not running.";
}
const std::vector<uint8_t> bytes = parse_address_hex(address_hex);
if (bytes.empty() || static_cast<int>(bytes.size()) != ADDRESS_BYTES) {
return "Error: Address must be 64 hex characters (32 bytes).";
}
auto [value, error] = get_balance(node, bytes.data(), nullptr);
if (!is_ok(&error)) {
return "Error: Failed to get balance: " + std::to_string(error);
}
return std::to_string(value);
}
std::string LogosBlockchainModule::wallet_transfer_funds(
const std::string& change_public_key,
const std::vector<std::string>& sender_addresses,
const std::string& recipient_address,
const std::string& amount,
const std::string& optional_tip_hex
) {
if (!node) {
return "Error: The node is not running.";
}
std::string amount_trimmed = amount;
boost::algorithm::trim(amount_trimmed);
uint64_t amount_val = 0;
auto [ptr, ec] = std::from_chars(amount_trimmed.data(), amount_trimmed.data() + amount_trimmed.size(), amount_val);
if (ec != std::errc{} || ptr != amount_trimmed.data() + amount_trimmed.size() || amount_trimmed.empty()) {
return "Error: Invalid amount (positive integer required).";
}
const std::vector<uint8_t> change_bytes = parse_address_hex(change_public_key);
if (change_bytes.empty() || static_cast<int>(change_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid change_public_key (64 hex characters required).";
}
const std::vector<uint8_t> recipient_bytes = parse_address_hex(recipient_address);
if (recipient_bytes.empty() || static_cast<int>(recipient_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid recipient_address (64 hex characters required).";
}
if (sender_addresses.empty()) {
return "Error: At least one sender address required.";
}
std::vector<std::vector<uint8_t>> funding_bytes;
for (const std::string& hex : sender_addresses) {
std::vector<uint8_t> b = parse_address_hex(hex);
if (b.empty() || static_cast<int>(b.size()) != ADDRESS_BYTES) {
return "Error: Invalid sender address (64 hex characters required).";
}
funding_bytes.push_back(std::move(b));
}
std::vector<const uint8_t*> funding_ptrs;
for (const auto& b : funding_bytes)
funding_ptrs.push_back(b.data());
std::vector<uint8_t> tip_bytes;
const HeaderId* optional_tip = nullptr;
if (!optional_tip_hex.empty()) {
tip_bytes = parse_address_hex(optional_tip_hex);
if (tip_bytes.empty() || static_cast<int>(tip_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid optional tip (64 hex characters or empty).";
}
optional_tip = reinterpret_cast<const HeaderId*>(tip_bytes.data());
}
TransferFundsArguments args{};
args.optional_tip = optional_tip;
args.change_public_key = change_bytes.data();
args.funding_public_keys = funding_ptrs.data();
args.funding_public_keys_len = funding_ptrs.size();
args.recipient_public_key = recipient_bytes.data();
args.amount = amount_val;
auto [value, error] = transfer_funds(node, &args);
if (!is_ok(&error)) {
return "Error: Failed to transfer funds: " + std::to_string(error);
}
return bytes_to_hex(reinterpret_cast<const uint8_t*>(&value), ADDRESS_BYTES);
}
std::vector<std::string> LogosBlockchainModule::wallet_get_known_addresses() {
std::vector<std::string> out;
if (!node) {
fprintf(stderr, "Could not execute the operation: The node is not running.\n");
return out;
}
auto [value, error] = get_known_addresses(node);
if (!is_ok(&error)) {
fprintf(stderr, "Failed to get known addresses. Error: %d\n", error);
return out;
}
for (size_t i = 0; i < value.len; ++i) {
const uint8_t* ptr = value.addresses[i];
if (ptr) {
out.push_back(bytes_to_hex(ptr, ADDRESS_BYTES));
}
}
const OperationStatus free_status = free_known_addresses(value);
if (!is_ok(&free_status)) {
fprintf(stderr, "Failed to free known addresses. Error: %d\n", free_status);
}
fprintf(stderr, "blockchain lib: known addresses, count=%zu sample:%s\n",
out.size(), out.empty() ? "(none)" : out.front().c_str());
return out;
}
std::string LogosBlockchainModule::wallet_get_notes(
const std::string& wallet_address_hex,
const std::string& optional_tip_hex
) {
if (!node) {
return "Error: The node is not running.";
}
const std::vector<uint8_t> address_bytes = parse_address_hex(wallet_address_hex);
if (address_bytes.empty() || static_cast<int>(address_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid wallet address (64 hex characters required).";
}
std::vector<uint8_t> tip_bytes;
const HeaderId* optional_tip = nullptr;
if (!optional_tip_hex.empty()) {
tip_bytes = parse_address_hex(optional_tip_hex);
if (tip_bytes.empty() || static_cast<int>(tip_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid optional tip (64 hex characters or empty).";
}
optional_tip = reinterpret_cast<const HeaderId*>(tip_bytes.data());
}
auto [value, error] = get_wallet_notes(node, address_bytes.data(), optional_tip);
if (!is_ok(&error)) {
return "Error: Failed to get wallet notes: " + std::to_string(error);
}
json obj;
obj["tip"] = bytes_to_hex(reinterpret_cast<const uint8_t*>(value.tip), TX_HASH_BYTES);
json notes = json::array();
for (size_t i = 0; i < value.len; ++i) {
const WalletNote& note = value.notes[i];
json n;
n["id"] = bytes_to_hex(reinterpret_cast<const uint8_t*>(note.id), TX_HASH_BYTES);
// Value is u64; serialized as a string to avoid JSON number precision loss.
n["value"] = std::to_string(note.value);
notes.push_back(std::move(n));
}
obj["notes"] = std::move(notes);
const OperationStatus free_status = free_wallet_notes(value);
if (!is_ok(&free_status)) {
fprintf(stderr, "Failed to free wallet notes. Error: %d\n", free_status);
}
return obj.dump();
}
std::string LogosBlockchainModule::leader_claim() {
if (!node) {
return "Error: The node is not running.";
}
auto [value, error] = ::leader_claim(node);
if (!is_ok(&error)) {
return "Error: Failed to claim leader rewards: " + std::to_string(error);
}
return bytes_to_hex(reinterpret_cast<const uint8_t*>(&value), TX_HASH_BYTES);
}
// Channel
std::string LogosBlockchainModule::channel_deposit(
const std::string& channel_id_hex,
const std::string& funding_public_key_hex,
const std::string& amount,
const std::string& metadata_hex,
const std::string& optional_tip_hex
) {
if (!node) {
return "Error: The node is not running.";
}
std::string amount_trimmed = amount;
boost::algorithm::trim(amount_trimmed);
uint64_t amount_val = 0;
auto [ptr, ec] = std::from_chars(amount_trimmed.data(), amount_trimmed.data() + amount_trimmed.size(), amount_val);
if (ec != std::errc{} || ptr != amount_trimmed.data() + amount_trimmed.size() || amount_trimmed.empty()) {
return "Error: Invalid amount (positive integer required).";
}
if (amount_val == 0) {
return "Error: Invalid amount (must be greater than zero).";
}
const std::vector<uint8_t> channel_bytes = parse_address_hex(channel_id_hex);
if (channel_bytes.empty() || static_cast<int>(channel_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid channel_id (64 hex characters required).";
}
const std::vector<uint8_t> funding_bytes = parse_address_hex(funding_public_key_hex);
if (funding_bytes.empty() || static_cast<int>(funding_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid funding_public_key (64 hex characters required).";
}
std::vector<uint8_t> metadata_bytes;
if (!metadata_hex.empty() && !parse_hex_bytes(metadata_hex, metadata_bytes)) {
return "Error: Invalid metadata (even-length hex string required).";
}
std::vector<uint8_t> tip_bytes;
const HeaderId* optional_tip = nullptr;
if (!optional_tip_hex.empty()) {
tip_bytes = parse_address_hex(optional_tip_hex);
if (tip_bytes.empty() || static_cast<int>(tip_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid optional tip (64 hex characters or empty).";
}
optional_tip = reinterpret_cast<const HeaderId*>(tip_bytes.data());
}
ChannelDepositArguments args{};
args.optional_tip = optional_tip;
args.channel_id = channel_bytes.data();
args.funding_public_key = funding_bytes.data();
args.amount = amount_val;
args.metadata = metadata_bytes.empty() ? nullptr : metadata_bytes.data();
args.metadata_len = metadata_bytes.size();
auto [value, error] = ::channel_deposit(node, &args);
if (!is_ok(&error)) {
return "Error: Failed to deposit into channel: " + std::to_string(error);
}
return bytes_to_hex(reinterpret_cast<const uint8_t*>(&value), ADDRESS_BYTES);
}
std::string LogosBlockchainModule::channel_deposit_with_notes(
const std::string& channel_id_hex,
const std::vector<std::string>& input_note_id_hexes,
const std::string& metadata_hex,
const std::string& change_public_key_hex,
const std::vector<std::string>& funding_public_key_hexes,
const std::string& max_tx_fee,
const std::string& optional_tip_hex
) {
if (!node) {
return "Error: The node is not running.";
}
const std::vector<uint8_t> channel_bytes = parse_address_hex(channel_id_hex);
if (channel_bytes.empty() || static_cast<int>(channel_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid channel_id (64 hex characters required).";
}
if (input_note_id_hexes.empty()) {
return "Error: At least one input note required.";
}
// Note IDs are 32-byte values stored contiguously so the buffer can be passed
// as a `NoteId` (uint8_t[32]) array.
std::vector<uint8_t> note_ids_flat;
note_ids_flat.reserve(input_note_id_hexes.size() * ADDRESS_BYTES);
for (const std::string& hex : input_note_id_hexes) {
const std::vector<uint8_t> b = parse_address_hex(hex);
if (b.empty() || static_cast<int>(b.size()) != ADDRESS_BYTES) {
return "Error: Invalid input note id (64 hex characters required).";
}
note_ids_flat.insert(note_ids_flat.end(), b.begin(), b.end());
}
const std::vector<uint8_t> change_bytes = parse_address_hex(change_public_key_hex);
if (change_bytes.empty() || static_cast<int>(change_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid change_public_key (64 hex characters required).";
}
if (funding_public_key_hexes.empty()) {
return "Error: At least one funding public key required.";
}
std::vector<std::vector<uint8_t>> funding_bytes;
for (const std::string& hex : funding_public_key_hexes) {
std::vector<uint8_t> b = parse_address_hex(hex);
if (b.empty() || static_cast<int>(b.size()) != ADDRESS_BYTES) {
return "Error: Invalid funding public key (64 hex characters required).";
}
funding_bytes.push_back(std::move(b));
}
std::vector<const uint8_t*> funding_ptrs;
funding_ptrs.reserve(funding_bytes.size());
for (const auto& b : funding_bytes)
funding_ptrs.push_back(b.data());
std::string fee_trimmed = max_tx_fee;
boost::algorithm::trim(fee_trimmed);
uint64_t max_tx_fee_val = 0;
auto [ptr, ec] = std::from_chars(fee_trimmed.data(), fee_trimmed.data() + fee_trimmed.size(), max_tx_fee_val);
if (ec != std::errc{} || ptr != fee_trimmed.data() + fee_trimmed.size() || fee_trimmed.empty()) {
return "Error: Invalid max_tx_fee (non-negative integer required).";
}
std::vector<uint8_t> metadata_bytes;
if (!metadata_hex.empty() && !parse_hex_bytes(metadata_hex, metadata_bytes)) {
return "Error: Invalid metadata (even-length hex string required).";
}
std::vector<uint8_t> tip_bytes;
const HeaderId* optional_tip = nullptr;
if (!optional_tip_hex.empty()) {
tip_bytes = parse_address_hex(optional_tip_hex);
if (tip_bytes.empty() || static_cast<int>(tip_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid optional tip (64 hex characters or empty).";
}
optional_tip = reinterpret_cast<const HeaderId*>(tip_bytes.data());
}
ChannelDepositWithNotesArguments args{};
args.optional_tip = optional_tip;
args.channel_id = channel_bytes.data();
args.input_note_ids = reinterpret_cast<const NoteId*>(note_ids_flat.data());
args.input_note_ids_len = input_note_id_hexes.size();
args.metadata = metadata_bytes.empty() ? nullptr : metadata_bytes.data();
args.metadata_len = metadata_bytes.size();
args.change_public_key = change_bytes.data();
args.funding_public_keys = funding_ptrs.data();
args.funding_public_keys_len = funding_ptrs.size();
args.max_tx_fee = max_tx_fee_val;
auto [value, error] = ::channel_deposit_with_notes(node, &args);
if (!is_ok(&error)) {
return "Error: Failed to deposit into channel: " + std::to_string(error);
}
return bytes_to_hex(reinterpret_cast<const uint8_t*>(&value), ADDRESS_BYTES);
}
std::string LogosBlockchainModule::wallet_get_claimable_vouchers() {
if (!node) {
return "Error: The node is not running.";
}
auto [value, error] = get_claimable_vouchers(node, nullptr);
if (!is_ok(&error)) {
return "Error: Failed to get claimable vouchers: " + std::to_string(error);
}
json obj;
obj["tip"] = bytes_to_hex(reinterpret_cast<const uint8_t*>(&value.tip), ADDRESS_BYTES);
obj["vouchers"] = json::array();
for (size_t i = 0; i < value.len; ++i) {
const ClaimableVoucher& voucher = value.vouchers[i];
obj["vouchers"].push_back({
{"commitment", bytes_to_hex(reinterpret_cast<const uint8_t*>(&voucher.commitment), ADDRESS_BYTES)},
{"nullifier", bytes_to_hex(reinterpret_cast<const uint8_t*>(&voucher.nullifier), ADDRESS_BYTES)},
});
}
const OperationStatus free_status = free_claimable_vouchers(value);
if (!is_ok(&free_status)) {
fprintf(stderr, "Failed to free claimable vouchers. Error: %d\n", free_status);
}
return obj.dump();
}
// Blend
std::string LogosBlockchainModule::blend_join_as_core_node(
const std::string& provider_id_hex,
const std::string& zk_id_hex,
const std::string& locked_note_id_hex,
const std::vector<std::string>& locators
) {
if (!node) {
return "Error: The node is not running.";
}
const std::vector<uint8_t> provider_id_bytes = parse_address_hex(provider_id_hex);
if (provider_id_bytes.empty() || static_cast<int>(provider_id_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid provider_id_hex (64 hex characters required).";
}
const std::vector<uint8_t> zk_id_bytes = parse_address_hex(zk_id_hex);
if (zk_id_bytes.empty() || static_cast<int>(zk_id_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid zk_id_hex (64 hex characters required).";
}
const std::vector<uint8_t> locked_note_id_bytes = parse_address_hex(locked_note_id_hex);
if (locked_note_id_bytes.empty() || static_cast<int>(locked_note_id_bytes.size()) != ADDRESS_BYTES) {
return "Error: Invalid locked_note_id_hex (64 hex characters required).";
}
// locators_ptrs holds raw pointers into the std::strings (valid as long as locators lives).
std::vector<const char*> locators_ptrs;
locators_ptrs.reserve(locators.size());
for (const std::string& locator : locators) {
locators_ptrs.push_back(locator.c_str());
}
auto [value, error] = ::blend_join_as_core_node(
node,
provider_id_bytes.data(),
zk_id_bytes.data(),
locked_note_id_bytes.data(),
locators_ptrs.data(),
locators_ptrs.size()
);
if (!is_ok(&error)) {
return "Error: Failed to join as core node: " + std::to_string(error);
}
std::string declaration_id = bytes_to_hex(reinterpret_cast<const uint8_t*>(&value), sizeof(value));
fprintf(stderr, "Successfully joined as core node. DeclarationId: %s\n", declaration_id.c_str());
return declaration_id;
}
// Explorer
std::string LogosBlockchainModule::get_block(const std::string& header_id_hex) {
if (!node) {
return "Error: The node is not running.";
}
const std::vector<uint8_t> bytes = parse_address_hex(header_id_hex);
if (bytes.empty() || static_cast<int>(bytes.size()) != ADDRESS_BYTES) {
return "Error: Header ID must be 64 hex characters (32 bytes).";
}
auto [value, error] = ::get_block(node, reinterpret_cast<const HeaderId*>(bytes.data()));
if (!is_ok(&error)) {
fprintf(stderr, "Failed to get block. Error: %d\n", error);
return "Error: Failed to get block: " + 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 block string. Error: %d\n", free_status);
}
return result;
}
std::string LogosBlockchainModule::get_blocks(const uint64_t from_slot, const uint64_t to_slot) {
if (!node) {
return "Error: The node is not running.";
}
auto [value, error] = ::get_blocks(node, from_slot, to_slot);
if (!is_ok(&error)) {
fprintf(stderr, "Failed to get blocks. Error: %d\n", error);
return "Error: Failed to get blocks: " + 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 blocks string. Error: %d\n", free_status);
}
return result;
}
std::string LogosBlockchainModule::get_transaction(const std::string& tx_hash_hex) {
if (!node) {
return "Error: The node is not running.";
}
const std::vector<uint8_t> bytes = parse_address_hex(tx_hash_hex);
if (bytes.empty() || static_cast<int>(bytes.size()) != ADDRESS_BYTES) {
return "Error: Transaction hash must be 64 hex characters (32 bytes).";
}
auto [value, error] = ::get_transaction(node, reinterpret_cast<const TxHash*>(bytes.data()));
if (!is_ok(&error)) {
fprintf(stderr, "Failed to get transaction. Error: %d\n", error);
return "Error: Failed to get transaction: " + 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 transaction string. Error: %d\n", free_status);
}
return result;
}
// Cryptarchia
std::string LogosBlockchainModule::get_cryptarchia_info() {
if (!node) {
return "Error: The node is not running.";
}
auto [value, error] = ::get_cryptarchia_info(node);
if (!is_ok(&error)) {
fprintf(stderr, "Failed to get cryptarchia info. Error: %d\n", error);
return "Error: Failed to get cryptarchia info: " + std::to_string(error);
}
json obj;
obj["lib"] = bytes_to_hex(reinterpret_cast<const uint8_t*>(value->lib), ADDRESS_BYTES);
obj["tip"] = bytes_to_hex(reinterpret_cast<const uint8_t*>(value->tip), ADDRESS_BYTES);
obj["slot"] = static_cast<int64_t>(value->slot);
obj["height"] = static_cast<int64_t>(value->height);
obj["mode"] = (value->mode == State::Online) ? "Online" : "Bootstrapping";
const OperationStatus free_status = free_cryptarchia_info(value);
if (!is_ok(&free_status)) {
fprintf(stderr, "Failed to free cryptarchia info. Error: %d\n", free_status);
}
return obj.dump();
}