mirror of
https://github.com/logos-blockchain/logos-blockchain-module.git
synced 2026-07-31 11:23:10 +00:00
feat: add missing ffi impl wrappers (#57)
This commit is contained in:
parent
581323e9e5
commit
df845c65a5
@ -3,7 +3,7 @@
|
||||
|
||||
inputs = {
|
||||
logos-module-builder.url = "github:logos-co/logos-module-builder/0.2.4";
|
||||
logos-blockchain.url = "github:logos-blockchain/logos-blockchain?ref=ec648fa7239576dccb9aa8b684d4a780c9064797";
|
||||
logos-blockchain.url = "github:logos-blockchain/logos-blockchain?ref=133570d58a141629a0c751fdf4412a66898acac2";
|
||||
};
|
||||
|
||||
outputs = inputs@{ logos-module-builder, ... }:
|
||||
|
||||
@ -263,15 +263,45 @@ namespace {
|
||||
} // namespace
|
||||
|
||||
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!
|
||||
if (!s_instance || !block) {
|
||||
return;
|
||||
}
|
||||
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!
|
||||
}
|
||||
|
||||
// The stream callbacks pass the FFI's JSON through unwrapped (it is already a
|
||||
// complete JSON document with the node's HTTP stream schema). A NULL pointer
|
||||
// means the stream ended; it is forwarded as the JSON literal `null` so
|
||||
// termination stays in-band on the same event.
|
||||
|
||||
void LogosBlockchainModule::on_processed_block_callback(const char* event) {
|
||||
if (!s_instance) {
|
||||
return;
|
||||
}
|
||||
if (!event) {
|
||||
fprintf(stderr, "Processed block stream ended.\n");
|
||||
s_instance->processedBlock("null");
|
||||
return;
|
||||
}
|
||||
s_instance->processedBlock(std::string(event));
|
||||
}
|
||||
|
||||
void LogosBlockchainModule::on_lib_block_callback(const char* event) {
|
||||
if (!s_instance) {
|
||||
return;
|
||||
}
|
||||
if (!event) {
|
||||
fprintf(stderr, "LIB block stream ended.\n");
|
||||
s_instance->libBlock("null");
|
||||
return;
|
||||
}
|
||||
s_instance->libBlock(std::string(event));
|
||||
}
|
||||
|
||||
LogosBlockchainModule::LogosBlockchainModule() {
|
||||
@ -414,7 +444,15 @@ StdLogosResult LogosBlockchainModule::start(const std::string& config_path, cons
|
||||
|
||||
s_instance = this;
|
||||
OperationStatus subscribe_status = subscribe_to_new_blocks(node, on_new_block_callback);
|
||||
return result::from_operation_status(subscribe_status);
|
||||
if (!is_ok(&subscribe_status)) {
|
||||
return result::err(operation_status::take_message(subscribe_status));
|
||||
}
|
||||
OperationStatus processed_status = subscribe_to_processed_blocks(node, on_processed_block_callback);
|
||||
if (!is_ok(&processed_status)) {
|
||||
return result::err(operation_status::take_message(processed_status));
|
||||
}
|
||||
OperationStatus lib_status = subscribe_to_lib_blocks(node, on_lib_block_callback);
|
||||
return result::from_operation_status(lib_status);
|
||||
}
|
||||
|
||||
StdLogosResult LogosBlockchainModule::stop() {
|
||||
@ -906,6 +944,31 @@ StdLogosResult LogosBlockchainModule::channel_deposit_with_notes(
|
||||
return result::ok(bytes_to_hex(reinterpret_cast<const uint8_t*>(&value), ADDRESS_BYTES));
|
||||
}
|
||||
|
||||
StdLogosResult LogosBlockchainModule::get_channel_state(const std::string& channel_id_hex) const {
|
||||
if (!node) {
|
||||
return result::err("The node is not running.");
|
||||
}
|
||||
|
||||
const std::vector<uint8_t> bytes = parse_address_hex(channel_id_hex);
|
||||
if (bytes.empty() || static_cast<int>(bytes.size()) != ADDRESS_BYTES) {
|
||||
return result::err("Invalid channel_id (64 hex characters required).");
|
||||
}
|
||||
|
||||
auto [value, error] = ::get_channel_state(node, bytes.data());
|
||||
if (!is_ok(&error)) {
|
||||
return result::err(operation_status::take_message(error));
|
||||
}
|
||||
|
||||
std::string out(value);
|
||||
OperationStatus free_status = free_cstring(value);
|
||||
if (!is_ok(&free_status)) {
|
||||
fprintf(
|
||||
stderr, "Failed to free channel state string: %s\n", operation_status::take_message(free_status).c_str()
|
||||
);
|
||||
}
|
||||
return result::ok(std::move(out));
|
||||
}
|
||||
|
||||
StdLogosResult LogosBlockchainModule::wallet_get_claimable_vouchers() const {
|
||||
if (!node) {
|
||||
return result::err("The node is not running.");
|
||||
@ -936,6 +999,38 @@ StdLogosResult LogosBlockchainModule::wallet_get_claimable_vouchers() const {
|
||||
return result::ok(obj.dump());
|
||||
}
|
||||
|
||||
StdLogosResult LogosBlockchainModule::wallet_fund_tx(const std::string& request_json) const {
|
||||
if (!node) {
|
||||
return result::err("The node is not running.");
|
||||
}
|
||||
|
||||
auto [value, error] = ::wallet_fund_tx(node, request_json.c_str());
|
||||
if (!is_ok(&error)) {
|
||||
return result::err(operation_status::take_message(error));
|
||||
}
|
||||
|
||||
std::string out(value);
|
||||
OperationStatus free_status = free_cstring(value);
|
||||
if (!is_ok(&free_status)) {
|
||||
fprintf(stderr, "Failed to free funded tx string: %s\n", operation_status::take_message(free_status).c_str());
|
||||
}
|
||||
return result::ok(std::move(out));
|
||||
}
|
||||
|
||||
// Transactions
|
||||
|
||||
StdLogosResult LogosBlockchainModule::submit_signed_transaction(const std::string& signed_tx_json) const {
|
||||
if (!node) {
|
||||
return result::err("The node is not running.");
|
||||
}
|
||||
|
||||
auto [value, error] = ::submit_signed_transaction(node, signed_tx_json.c_str());
|
||||
if (!is_ok(&error)) {
|
||||
return result::err(operation_status::take_message(error));
|
||||
}
|
||||
return result::ok(bytes_to_hex(reinterpret_cast<const uint8_t*>(&value), TX_HASH_BYTES));
|
||||
}
|
||||
|
||||
// Blend
|
||||
|
||||
StdLogosResult LogosBlockchainModule::blend_join_as_core_node(
|
||||
@ -955,11 +1050,7 @@ StdLogosResult LogosBlockchainModule::blend_join_as_core_node(
|
||||
return result::err("Invalid locked_note_id_hex (64 hex characters required).");
|
||||
}
|
||||
|
||||
auto [value, error] = ::blend_join_as_core_node(
|
||||
node,
|
||||
locator.c_str(),
|
||||
locked_note_id_bytes.data()
|
||||
);
|
||||
auto [value, error] = ::blend_join_as_core_node(node, locator.c_str(), locked_note_id_bytes.data());
|
||||
if (!is_ok(&error)) {
|
||||
return result::err(operation_status::take_message(error));
|
||||
}
|
||||
@ -1049,10 +1140,21 @@ StdLogosResult LogosBlockchainModule::get_cryptarchia_info() const {
|
||||
|
||||
json obj;
|
||||
obj["lib"] = bytes_to_hex(reinterpret_cast<const uint8_t*>(value->lib), ADDRESS_BYTES);
|
||||
obj["lib_slot"] = static_cast<int64_t>(value->lib_slot);
|
||||
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";
|
||||
switch (value->mode) {
|
||||
case State::Online:
|
||||
obj["mode"] = "Online";
|
||||
break;
|
||||
case State::NotStarted:
|
||||
obj["mode"] = "NotStarted";
|
||||
break;
|
||||
default:
|
||||
obj["mode"] = "Bootstrapping";
|
||||
break;
|
||||
}
|
||||
|
||||
OperationStatus free_status = free_cryptarchia_info(value);
|
||||
if (!is_ok(&free_status)) {
|
||||
@ -1060,3 +1162,53 @@ StdLogosResult LogosBlockchainModule::get_cryptarchia_info() const {
|
||||
}
|
||||
return result::ok(obj.dump());
|
||||
}
|
||||
|
||||
StdLogosResult LogosBlockchainModule::get_block_events(const std::string& header_id_hex) const {
|
||||
if (!node) {
|
||||
return result::err("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 result::err("Header ID must be 64 hex characters (32 bytes).");
|
||||
}
|
||||
|
||||
auto [value, error] = ::get_block_events(node, reinterpret_cast<const HeaderId*>(bytes.data()));
|
||||
if (!is_ok(&error)) {
|
||||
return result::err(operation_status::take_message(error));
|
||||
}
|
||||
|
||||
std::string out(value);
|
||||
OperationStatus free_status = free_cstring(value);
|
||||
if (!is_ok(&free_status)) {
|
||||
fprintf(
|
||||
stderr, "Failed to free block events string: %s\n", operation_status::take_message(free_status).c_str()
|
||||
);
|
||||
}
|
||||
return result::ok(std::move(out));
|
||||
}
|
||||
|
||||
// Time
|
||||
|
||||
StdLogosResult LogosBlockchainModule::get_time_info() const {
|
||||
if (!node) {
|
||||
return result::err("The node is not running.");
|
||||
}
|
||||
|
||||
auto [value, error] = ::get_time_info(node);
|
||||
if (!is_ok(&error)) {
|
||||
return result::err(operation_status::take_message(error));
|
||||
}
|
||||
|
||||
json obj;
|
||||
obj["slot_duration_ms"] = static_cast<int64_t>(value->slot_duration_ms);
|
||||
obj["genesis_time_unix_ms"] = value->genesis_time_unix_ms;
|
||||
obj["current_slot"] = static_cast<int64_t>(value->current_slot);
|
||||
obj["current_epoch"] = value->current_epoch;
|
||||
|
||||
OperationStatus free_status = free_time_info(value);
|
||||
if (!is_ok(&free_status)) {
|
||||
fprintf(stderr, "Failed to free time info: %s\n", operation_status::take_message(free_status).c_str());
|
||||
}
|
||||
return result::ok(obj.dump());
|
||||
}
|
||||
|
||||
@ -99,6 +99,16 @@ public:
|
||||
) const;
|
||||
[[nodiscard]] StdLogosResult leader_claim() const;
|
||||
[[nodiscard]] StdLogosResult wallet_get_claimable_vouchers() const;
|
||||
// Funds an unsigned transaction: request_json is passed through to the
|
||||
// node's wallet fund endpoint (same JSON schema as the HTTP `/wallet/fund`
|
||||
// request body); returns the funded transaction as JSON.
|
||||
[[nodiscard]] StdLogosResult wallet_fund_tx(const std::string& request_json) const;
|
||||
|
||||
// Transactions
|
||||
// Submits a signed transaction: signed_tx_json is passed through to the
|
||||
// node (same JSON schema as the HTTP `/mantle/transact` request body);
|
||||
// returns the transaction hash hex on success.
|
||||
[[nodiscard]] StdLogosResult submit_signed_transaction(const std::string& signed_tx_json) const;
|
||||
|
||||
// Channel
|
||||
// Amount-based deposit: the binding selects funding notes itself (splitting a
|
||||
@ -128,6 +138,11 @@ public:
|
||||
const std::string& optional_tip_hex
|
||||
) const;
|
||||
|
||||
// State of the channel with the given 32-byte channel ID, as JSON (same
|
||||
// schema as the node's `/mantle/channel/{id}` HTTP endpoint). Fails with a
|
||||
// not-found error when the channel does not exist yet.
|
||||
[[nodiscard]] StdLogosResult get_channel_state(const std::string& channel_id_hex) const;
|
||||
|
||||
// Blend
|
||||
[[nodiscard]] StdLogosResult blend_join_as_core_node(
|
||||
const std::string& locator,
|
||||
@ -141,6 +156,13 @@ public:
|
||||
|
||||
// Cryptarchia
|
||||
[[nodiscard]] StdLogosResult get_cryptarchia_info() const;
|
||||
// Events emitted by the block with the given 32-byte header ID, as JSON.
|
||||
[[nodiscard]] StdLogosResult get_block_events(const std::string& header_id_hex) const;
|
||||
|
||||
// Time
|
||||
// Consensus time info as JSON:
|
||||
// { slot_duration_ms, genesis_time_unix_ms, current_slot, current_epoch }
|
||||
[[nodiscard]] StdLogosResult get_time_info() const;
|
||||
|
||||
// clang-format off
|
||||
// Clang-format only handles public/private/protected, so it miss-indents this section.
|
||||
@ -150,6 +172,19 @@ logos_events:
|
||||
// blockJson is the full block serialized as JSON.
|
||||
// ReSharper disable once CppFunctionIsNotImplemented
|
||||
void newBlock(const std::string& blockJson);
|
||||
// Fired per processed block. eventJson carries the block plus the chain
|
||||
// state after processing it (same schema as the node's
|
||||
// `/cryptarchia/blocks/stream` HTTP endpoint; transaction ids at
|
||||
// `transactions[].mantle_tx.hash`). When the stream ends, fired exactly
|
||||
// once with the JSON literal `null` — restart the node subscription (via
|
||||
// stop/start) to keep receiving events.
|
||||
// ReSharper disable once CppFunctionIsNotImplemented
|
||||
void processedBlock(const std::string& eventJson);
|
||||
// Fired per newly finalized (LIB) block. blockInfoJson uses the same
|
||||
// schema as the node's `/cryptarchia/lib/stream` HTTP endpoint. When the
|
||||
// stream ends, fired exactly once with the JSON literal `null`.
|
||||
// ReSharper disable once CppFunctionIsNotImplemented
|
||||
void libBlock(const std::string& blockInfoJson);
|
||||
// clang-format on
|
||||
|
||||
private:
|
||||
@ -158,6 +193,10 @@ private:
|
||||
// Static instance for C callback (C API doesn't support user data)
|
||||
static LogosBlockchainModule* s_instance;
|
||||
|
||||
// C-compatible callback function
|
||||
// C-compatible callback functions. The stream callbacks receive NULL
|
||||
// exactly once when their stream ends; that is forwarded as the JSON
|
||||
// literal `null` on the corresponding event.
|
||||
static void on_new_block_callback(const char* block);
|
||||
static void on_processed_block_callback(const char* event);
|
||||
static void on_lib_block_callback(const char* event);
|
||||
};
|
||||
|
||||
@ -12,6 +12,23 @@
|
||||
|
||||
#include "logos_blockchain_module.h"
|
||||
|
||||
// Recorded payloads of the most recent stream events, so tests can assert what
|
||||
// the trampolines forwarded (including the `null` end-of-stream sentinel).
|
||||
std::string g_lastNewBlockEventJson;
|
||||
std::string g_lastProcessedBlockEventJson;
|
||||
std::string g_lastLibBlockEventJson;
|
||||
|
||||
void LogosBlockchainModule::newBlock(const std::string& blockJson) {
|
||||
g_lastNewBlockEventJson = blockJson;
|
||||
emitEventImpl_("newBlock", nullptr);
|
||||
}
|
||||
|
||||
void LogosBlockchainModule::processedBlock(const std::string& eventJson) {
|
||||
g_lastProcessedBlockEventJson = eventJson;
|
||||
emitEventImpl_("processedBlock", nullptr);
|
||||
}
|
||||
|
||||
void LogosBlockchainModule::libBlock(const std::string& blockInfoJson) {
|
||||
g_lastLibBlockEventJson = blockInfoJson;
|
||||
emitEventImpl_("libBlock", nullptr);
|
||||
}
|
||||
|
||||
@ -16,6 +16,12 @@ std::string g_lastGeneratedStatePath;
|
||||
std::string g_lastGeneratedStoragePath;
|
||||
std::string g_lastGeneratedLogsPath;
|
||||
|
||||
// Captures the callbacks passed to the subscription calls so tests can drive
|
||||
// the streams (including the NULL end-of-stream sentinel).
|
||||
BlockCallback g_lastNewBlockCallback = nullptr;
|
||||
BlockCallback g_lastProcessedBlockCallback = nullptr;
|
||||
BlockCallback g_lastLibBlockCallback = nullptr;
|
||||
|
||||
static char s_fakeNode = 0;
|
||||
static CryptarchiaInfo s_fakeCryptarchiaInfo = {};
|
||||
|
||||
@ -137,9 +143,22 @@ StringResult get_peer_id(const char* config_path) {
|
||||
|
||||
OperationStatus subscribe_to_new_blocks(LogosBlockchainNode* node, BlockCallback callback) {
|
||||
LOGOS_CMOCK_RECORD("subscribe_to_new_blocks");
|
||||
g_lastNewBlockCallback = callback;
|
||||
return make_status(LOGOS_CMOCK_RETURN(int, "subscribe_to_new_blocks"));
|
||||
}
|
||||
|
||||
OperationStatus subscribe_to_processed_blocks(LogosBlockchainNode* node, BlockCallback callback) {
|
||||
LOGOS_CMOCK_RECORD("subscribe_to_processed_blocks");
|
||||
g_lastProcessedBlockCallback = callback;
|
||||
return make_status(LOGOS_CMOCK_RETURN(int, "subscribe_to_processed_blocks"));
|
||||
}
|
||||
|
||||
OperationStatus subscribe_to_lib_blocks(LogosBlockchainNode* node, BlockCallback callback) {
|
||||
LOGOS_CMOCK_RECORD("subscribe_to_lib_blocks");
|
||||
g_lastLibBlockCallback = callback;
|
||||
return make_status(LOGOS_CMOCK_RETURN(int, "subscribe_to_lib_blocks"));
|
||||
}
|
||||
|
||||
BalanceResult get_balance(LogosBlockchainNode* node, const uint8_t* address, const void* reserved) {
|
||||
LOGOS_CMOCK_RECORD("get_balance");
|
||||
BalanceResult result;
|
||||
@ -218,6 +237,15 @@ OperationStatus free_claimable_vouchers(ClaimableVouchers vouchers) {
|
||||
return make_status(0);
|
||||
}
|
||||
|
||||
StringResult get_channel_state(LogosBlockchainNode* node, const uint8_t* channel_id) {
|
||||
LOGOS_CMOCK_RECORD("get_channel_state");
|
||||
StringResult result;
|
||||
const char* json = LOGOS_CMOCK_RETURN_STRING("get_channel_state");
|
||||
result.value = json ? strdup(json) : nullptr;
|
||||
result.error = make_status(LOGOS_CMOCK_RETURN(int, "get_channel_state_error"));
|
||||
return result;
|
||||
}
|
||||
|
||||
// Wallet-notes mock storage (up to 4 notes)
|
||||
static WalletNote s_mockNotes[4];
|
||||
|
||||
@ -251,6 +279,23 @@ OperationStatus free_wallet_notes(WalletNotes notes) {
|
||||
return make_status(0);
|
||||
}
|
||||
|
||||
StringResult wallet_fund_tx(LogosBlockchainNode* node, const char* request_json) {
|
||||
LOGOS_CMOCK_RECORD("wallet_fund_tx");
|
||||
StringResult result;
|
||||
const char* json = LOGOS_CMOCK_RETURN_STRING("wallet_fund_tx");
|
||||
result.value = json ? strdup(json) : nullptr;
|
||||
result.error = make_status(LOGOS_CMOCK_RETURN(int, "wallet_fund_tx_error"));
|
||||
return result;
|
||||
}
|
||||
|
||||
SubmitTransactionResult submit_signed_transaction(LogosBlockchainNode* node, const char* signed_tx_json) {
|
||||
LOGOS_CMOCK_RECORD("submit_signed_transaction");
|
||||
SubmitTransactionResult result;
|
||||
memset(result.value, 0xFA, sizeof(Hash));
|
||||
result.error = make_status(LOGOS_CMOCK_RETURN(int, "submit_signed_transaction_error"));
|
||||
return result;
|
||||
}
|
||||
|
||||
FfiChannelDepositResult channel_deposit(LogosBlockchainNode* node, const ChannelDepositArguments* arguments) {
|
||||
LOGOS_CMOCK_RECORD("channel_deposit");
|
||||
FfiChannelDepositResult result;
|
||||
@ -313,6 +358,7 @@ CryptarchiaInfoResult get_cryptarchia_info(LogosBlockchainNode* node) {
|
||||
LOGOS_CMOCK_RECORD("get_cryptarchia_info");
|
||||
CryptarchiaInfoResult result;
|
||||
memset(&s_fakeCryptarchiaInfo, 0, sizeof(s_fakeCryptarchiaInfo));
|
||||
s_fakeCryptarchiaInfo.lib_slot = static_cast<uint64_t>(LOGOS_CMOCK_RETURN(int, "cryptarchia_lib_slot"));
|
||||
s_fakeCryptarchiaInfo.slot = static_cast<uint64_t>(LOGOS_CMOCK_RETURN(int, "cryptarchia_slot"));
|
||||
s_fakeCryptarchiaInfo.height = static_cast<uint64_t>(LOGOS_CMOCK_RETURN(int, "cryptarchia_height"));
|
||||
s_fakeCryptarchiaInfo.mode = static_cast<State>(LOGOS_CMOCK_RETURN(int, "cryptarchia_mode"));
|
||||
@ -328,6 +374,34 @@ OperationStatus free_cryptarchia_info(CryptarchiaInfo* info) {
|
||||
return make_status(0);
|
||||
}
|
||||
|
||||
StringResult get_block_events(LogosBlockchainNode* node, const HeaderId* header_id) {
|
||||
LOGOS_CMOCK_RECORD("get_block_events");
|
||||
StringResult result;
|
||||
const char* json = LOGOS_CMOCK_RETURN_STRING("get_block_events");
|
||||
result.value = json ? strdup(json) : nullptr;
|
||||
result.error = make_status(LOGOS_CMOCK_RETURN(int, "get_block_events_error"));
|
||||
return result;
|
||||
}
|
||||
|
||||
static TimeInfo s_fakeTimeInfo = {};
|
||||
|
||||
TimeInfoResult get_time_info(LogosBlockchainNode* node) {
|
||||
LOGOS_CMOCK_RECORD("get_time_info");
|
||||
TimeInfoResult result;
|
||||
s_fakeTimeInfo.slot_duration_ms = static_cast<uint64_t>(LOGOS_CMOCK_RETURN(int, "time_slot_duration_ms"));
|
||||
s_fakeTimeInfo.genesis_time_unix_ms = static_cast<int64_t>(LOGOS_CMOCK_RETURN(int, "time_genesis_time_unix_ms"));
|
||||
s_fakeTimeInfo.current_slot = static_cast<uint64_t>(LOGOS_CMOCK_RETURN(int, "time_current_slot"));
|
||||
s_fakeTimeInfo.current_epoch = static_cast<uint32_t>(LOGOS_CMOCK_RETURN(int, "time_current_epoch"));
|
||||
result.value = &s_fakeTimeInfo;
|
||||
result.error = make_status(LOGOS_CMOCK_RETURN(int, "get_time_info_error"));
|
||||
return result;
|
||||
}
|
||||
|
||||
OperationStatus free_time_info(TimeInfo* info) {
|
||||
LOGOS_CMOCK_RECORD("free_time_info");
|
||||
return make_status(0);
|
||||
}
|
||||
|
||||
OperationStatus free_cstring(char* s) {
|
||||
LOGOS_CMOCK_RECORD("free_cstring");
|
||||
free(s);
|
||||
|
||||
@ -45,7 +45,7 @@ typedef struct {
|
||||
} OperationStatus;
|
||||
|
||||
// Consensus state enum
|
||||
typedef enum { Bootstrapping, Online } State;
|
||||
typedef enum { Bootstrapping, Online, NotStarted } State;
|
||||
|
||||
// Key type for generate_key / add_key
|
||||
typedef enum { Ed25519, Zk } KeyType;
|
||||
@ -134,12 +134,21 @@ typedef struct {
|
||||
// Cryptarchia consensus info
|
||||
typedef struct {
|
||||
uint8_t lib[32];
|
||||
uint64_t lib_slot;
|
||||
uint8_t tip[32];
|
||||
uint64_t slot;
|
||||
uint64_t height;
|
||||
State mode;
|
||||
} CryptarchiaInfo;
|
||||
|
||||
// Time service info
|
||||
typedef struct {
|
||||
uint64_t slot_duration_ms;
|
||||
int64_t genesis_time_unix_ms;
|
||||
uint64_t current_slot;
|
||||
uint32_t current_epoch;
|
||||
} TimeInfo;
|
||||
|
||||
// Result types (C++ structured bindings decompose these)
|
||||
typedef struct { LogosBlockchainNode* value; OperationStatus error; } NodeResult;
|
||||
typedef struct { uint64_t value; OperationStatus error; } BalanceResult;
|
||||
@ -152,6 +161,8 @@ typedef struct { ClaimableVouchers value; OperationStatus error; } FfiClaimableV
|
||||
typedef struct { Hash value; OperationStatus error; } BlendHashResult;
|
||||
typedef struct { char* value; OperationStatus error; } StringResult;
|
||||
typedef struct { CryptarchiaInfo* value; OperationStatus error; } CryptarchiaInfoResult;
|
||||
typedef struct { TimeInfo* value; OperationStatus error; } TimeInfoResult;
|
||||
typedef struct { Hash value; OperationStatus error; } SubmitTransactionResult;
|
||||
|
||||
// Block event callback
|
||||
typedef void (*BlockCallback)(const char* block_json);
|
||||
@ -164,6 +175,10 @@ OperationStatus generate_user_config(GenerateConfigArgs args);
|
||||
NodeResult start_lb_node(const char* config_path, const char* deployment);
|
||||
OperationStatus shutdown_node(LogosBlockchainNode* node);
|
||||
OperationStatus subscribe_to_new_blocks(LogosBlockchainNode* node, BlockCallback callback);
|
||||
// Streams: each event is a JSON C string; the callback is invoked exactly once
|
||||
// with NULL when the stream ends.
|
||||
OperationStatus subscribe_to_processed_blocks(LogosBlockchainNode* node, BlockCallback callback);
|
||||
OperationStatus subscribe_to_lib_blocks(LogosBlockchainNode* node, BlockCallback callback);
|
||||
|
||||
// Config management
|
||||
OperationStatus update_user_config(const char* user_config_path, const char* keystore_path);
|
||||
@ -209,6 +224,10 @@ FfiWalletNotesResult get_wallet_notes(
|
||||
const uint8_t* wallet_address,
|
||||
const HeaderId* optional_tip);
|
||||
OperationStatus free_wallet_notes(WalletNotes notes);
|
||||
StringResult wallet_fund_tx(LogosBlockchainNode* node, const char* request_json);
|
||||
|
||||
// Transactions
|
||||
SubmitTransactionResult submit_signed_transaction(LogosBlockchainNode* node, const char* signed_tx_json);
|
||||
|
||||
// Channel
|
||||
FfiChannelDepositResult channel_deposit(LogosBlockchainNode* node, const ChannelDepositArguments* arguments);
|
||||
@ -217,6 +236,7 @@ FfiChannelDepositResult channel_deposit_with_notes(
|
||||
const ChannelDepositWithNotesArguments* arguments);
|
||||
FfiClaimableVouchersResult get_claimable_vouchers(LogosBlockchainNode* node, const HeaderId* optional_tip);
|
||||
OperationStatus free_claimable_vouchers(ClaimableVouchers vouchers);
|
||||
StringResult get_channel_state(LogosBlockchainNode* node, const uint8_t* channel_id);
|
||||
|
||||
// Blend
|
||||
BlendHashResult blend_join_as_core_node(
|
||||
@ -232,6 +252,11 @@ StringResult get_transaction(LogosBlockchainNode* node, const TxHash* tx_hash);
|
||||
// Cryptarchia
|
||||
CryptarchiaInfoResult get_cryptarchia_info(LogosBlockchainNode* node);
|
||||
OperationStatus free_cryptarchia_info(CryptarchiaInfo* info);
|
||||
StringResult get_block_events(LogosBlockchainNode* node, const HeaderId* header_id);
|
||||
|
||||
// Time
|
||||
TimeInfoResult get_time_info(LogosBlockchainNode* node);
|
||||
OperationStatus free_time_info(TimeInfo* info);
|
||||
|
||||
// Memory management
|
||||
OperationStatus free_cstring(char* s);
|
||||
|
||||
@ -321,6 +321,42 @@ LOGOS_TEST(get_cryptarchia_info_without_node_returns_error) {
|
||||
LOGOS_ASSERT_FALSE(module.get_cryptarchia_info().success);
|
||||
}
|
||||
|
||||
LOGOS_TEST(get_block_events_without_node_returns_error) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
LogosBlockchainModule module;
|
||||
LOGOS_ASSERT_FALSE(module.get_block_events(VALID_HEX).success);
|
||||
}
|
||||
|
||||
LOGOS_TEST(get_time_info_without_node_returns_error) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
LogosBlockchainModule module;
|
||||
LOGOS_ASSERT_FALSE(module.get_time_info().success);
|
||||
}
|
||||
|
||||
LOGOS_TEST(get_channel_state_without_node_returns_error) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
LogosBlockchainModule module;
|
||||
StdLogosResult result = module.get_channel_state(VALID_HEX);
|
||||
LOGOS_ASSERT_FALSE(result.success);
|
||||
LOGOS_ASSERT_TRUE(contains(result.error, "not running"));
|
||||
}
|
||||
|
||||
LOGOS_TEST(wallet_fund_tx_without_node_returns_error) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
LogosBlockchainModule module;
|
||||
StdLogosResult result = module.wallet_fund_tx("{}");
|
||||
LOGOS_ASSERT_FALSE(result.success);
|
||||
LOGOS_ASSERT_TRUE(contains(result.error, "not running"));
|
||||
}
|
||||
|
||||
LOGOS_TEST(submit_signed_transaction_without_node_returns_error) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
LogosBlockchainModule module;
|
||||
StdLogosResult result = module.submit_signed_transaction("{}");
|
||||
LOGOS_ASSERT_FALSE(result.success);
|
||||
LOGOS_ASSERT_TRUE(contains(result.error, "not running"));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Node lifecycle (start / stop)
|
||||
// ============================================================================
|
||||
@ -334,9 +370,36 @@ LOGOS_TEST(start_succeeds_with_mocked_dependencies) {
|
||||
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||
LOGOS_ASSERT(t.cFunctionCalled("start_lb_node"));
|
||||
LOGOS_ASSERT(t.cFunctionCalled("subscribe_to_new_blocks"));
|
||||
LOGOS_ASSERT(t.cFunctionCalled("subscribe_to_processed_blocks"));
|
||||
LOGOS_ASSERT(t.cFunctionCalled("subscribe_to_lib_blocks"));
|
||||
delete module;
|
||||
}
|
||||
|
||||
LOGOS_TEST(start_fails_when_processed_blocks_subscription_fails) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
TempDir tmpDir;
|
||||
LogosBlockchainModule module;
|
||||
|
||||
t.mockCFunction("start_lb_node").returns(1);
|
||||
t.mockCFunction("subscribe_to_new_blocks").returns(0);
|
||||
t.mockCFunction("subscribe_to_processed_blocks").returns(1);
|
||||
|
||||
LOGOS_ASSERT_FALSE(module.start(tmpDir.filePath("config.json"), "").success);
|
||||
}
|
||||
|
||||
LOGOS_TEST(start_fails_when_lib_blocks_subscription_fails) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
TempDir tmpDir;
|
||||
LogosBlockchainModule module;
|
||||
|
||||
t.mockCFunction("start_lb_node").returns(1);
|
||||
t.mockCFunction("subscribe_to_new_blocks").returns(0);
|
||||
t.mockCFunction("subscribe_to_processed_blocks").returns(0);
|
||||
t.mockCFunction("subscribe_to_lib_blocks").returns(1);
|
||||
|
||||
LOGOS_ASSERT_FALSE(module.start(tmpDir.filePath("config.json"), "").success);
|
||||
}
|
||||
|
||||
LOGOS_TEST(start_returns_1_when_already_running) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
TempDir tmpDir;
|
||||
@ -525,6 +588,30 @@ LOGOS_TEST(get_transaction_rejects_invalid_hex) {
|
||||
delete module;
|
||||
}
|
||||
|
||||
LOGOS_TEST(get_block_events_rejects_invalid_hex) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
TempDir tmpDir;
|
||||
auto* module = createStartedModule(t, tmpDir);
|
||||
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||
|
||||
StdLogosResult result = module->get_block_events("tooshort");
|
||||
LOGOS_ASSERT_FALSE(result.success);
|
||||
LOGOS_ASSERT_TRUE(contains(result.error, "64 hex"));
|
||||
delete module;
|
||||
}
|
||||
|
||||
LOGOS_TEST(get_channel_state_rejects_invalid_hex) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
TempDir tmpDir;
|
||||
auto* module = createStartedModule(t, tmpDir);
|
||||
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||
|
||||
StdLogosResult result = module->get_channel_state("bad");
|
||||
LOGOS_ASSERT_FALSE(result.success);
|
||||
LOGOS_ASSERT_TRUE(contains(result.error, "channel_id"));
|
||||
delete module;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 0x prefix handling
|
||||
// ============================================================================
|
||||
@ -1013,6 +1100,104 @@ LOGOS_TEST(wallet_get_claimable_vouchers_returns_error_on_ffi_failure) {
|
||||
delete module;
|
||||
}
|
||||
|
||||
LOGOS_TEST(wallet_fund_tx_returns_json_on_success) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
TempDir tmpDir;
|
||||
auto* module = createStartedModule(t, tmpDir);
|
||||
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||
|
||||
t.mockCFunction("wallet_fund_tx").returns(R"({"mantle_tx":{"ops":[]}})");
|
||||
t.mockCFunction("wallet_fund_tx_error").returns(0);
|
||||
|
||||
StdLogosResult result = module->wallet_fund_tx(R"({"tx":{},"funding_keys":[]})");
|
||||
LOGOS_ASSERT_TRUE(result.success);
|
||||
LOGOS_ASSERT_TRUE(contains(result.value.get<std::string>(), "mantle_tx"));
|
||||
LOGOS_ASSERT(t.cFunctionCalled("wallet_fund_tx"));
|
||||
LOGOS_ASSERT(t.cFunctionCalled("free_cstring"));
|
||||
delete module;
|
||||
}
|
||||
|
||||
LOGOS_TEST(wallet_fund_tx_returns_error_on_ffi_failure) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
TempDir tmpDir;
|
||||
auto* module = createStartedModule(t, tmpDir);
|
||||
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||
|
||||
t.mockCFunction("wallet_fund_tx_error").returns(1);
|
||||
|
||||
StdLogosResult result = module->wallet_fund_tx("{}");
|
||||
LOGOS_ASSERT_FALSE(result.success);
|
||||
LOGOS_ASSERT_TRUE(contains(result.error, "mock error"));
|
||||
delete module;
|
||||
}
|
||||
|
||||
// Transactions
|
||||
|
||||
LOGOS_TEST(submit_signed_transaction_returns_tx_hash) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
TempDir tmpDir;
|
||||
auto* module = createStartedModule(t, tmpDir);
|
||||
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||
|
||||
t.mockCFunction("submit_signed_transaction_error").returns(0);
|
||||
|
||||
StdLogosResult result = module->submit_signed_transaction("{}");
|
||||
LOGOS_ASSERT_TRUE(result.success);
|
||||
// Mock fills hash with 0xFA -> hex "fafa...fa" (64 chars)
|
||||
std::string hash = result.value.get<std::string>();
|
||||
LOGOS_ASSERT_EQ(static_cast<int>(hash.length()), 64);
|
||||
LOGOS_ASSERT_TRUE(hash.substr(0, 2) == "fa");
|
||||
LOGOS_ASSERT(t.cFunctionCalled("submit_signed_transaction"));
|
||||
delete module;
|
||||
}
|
||||
|
||||
LOGOS_TEST(submit_signed_transaction_returns_error_on_ffi_failure) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
TempDir tmpDir;
|
||||
auto* module = createStartedModule(t, tmpDir);
|
||||
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||
|
||||
t.mockCFunction("submit_signed_transaction_error").returns(1);
|
||||
|
||||
StdLogosResult result = module->submit_signed_transaction("{}");
|
||||
LOGOS_ASSERT_FALSE(result.success);
|
||||
LOGOS_ASSERT_TRUE(contains(result.error, "mock error"));
|
||||
delete module;
|
||||
}
|
||||
|
||||
// Channel state
|
||||
|
||||
LOGOS_TEST(get_channel_state_returns_json_on_success) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
TempDir tmpDir;
|
||||
auto* module = createStartedModule(t, tmpDir);
|
||||
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||
|
||||
t.mockCFunction("get_channel_state").returns(R"({"tip":"abc","inscriptions":[]})");
|
||||
t.mockCFunction("get_channel_state_error").returns(0);
|
||||
|
||||
StdLogosResult result = module->get_channel_state(VALID_HEX);
|
||||
LOGOS_ASSERT_TRUE(result.success);
|
||||
LOGOS_ASSERT_TRUE(contains(result.value.get<std::string>(), "inscriptions"));
|
||||
LOGOS_ASSERT(t.cFunctionCalled("get_channel_state"));
|
||||
LOGOS_ASSERT(t.cFunctionCalled("free_cstring"));
|
||||
delete module;
|
||||
}
|
||||
|
||||
LOGOS_TEST(get_channel_state_returns_error_on_ffi_failure) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
TempDir tmpDir;
|
||||
auto* module = createStartedModule(t, tmpDir);
|
||||
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||
|
||||
t.mockCFunction("get_channel_state_error").returns(1);
|
||||
|
||||
StdLogosResult result = module->get_channel_state(VALID_HEX);
|
||||
LOGOS_ASSERT_FALSE(result.success);
|
||||
LOGOS_ASSERT_TRUE(contains(result.error, "mock error"));
|
||||
delete module;
|
||||
}
|
||||
|
||||
// Blend
|
||||
|
||||
LOGOS_TEST(blend_join_as_core_node_returns_declaration_id) {
|
||||
@ -1146,6 +1331,7 @@ LOGOS_TEST(get_cryptarchia_info_returns_json_on_success) {
|
||||
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||
|
||||
t.mockCFunction("get_cryptarchia_info_error").returns(0);
|
||||
t.mockCFunction("cryptarchia_lib_slot").returns(90);
|
||||
t.mockCFunction("cryptarchia_slot").returns(100);
|
||||
t.mockCFunction("cryptarchia_height").returns(50);
|
||||
t.mockCFunction("cryptarchia_mode").returns(1); // Online
|
||||
@ -1160,11 +1346,26 @@ LOGOS_TEST(get_cryptarchia_info_returns_json_on_success) {
|
||||
LOGOS_ASSERT_TRUE(contains(json, "Online"));
|
||||
LOGOS_ASSERT_TRUE(contains(json, "lib"));
|
||||
LOGOS_ASSERT_TRUE(contains(json, "tip"));
|
||||
LOGOS_ASSERT_TRUE(contains(json, "\"lib_slot\":90"));
|
||||
LOGOS_ASSERT(t.cFunctionCalled("get_cryptarchia_info"));
|
||||
LOGOS_ASSERT(t.cFunctionCalled("free_cryptarchia_info"));
|
||||
delete module;
|
||||
}
|
||||
|
||||
LOGOS_TEST(get_cryptarchia_info_not_started_mode) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
TempDir tmpDir;
|
||||
auto* module = createStartedModule(t, tmpDir);
|
||||
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||
|
||||
t.mockCFunction("get_cryptarchia_info_error").returns(0);
|
||||
t.mockCFunction("cryptarchia_mode").returns(2); // NotStarted
|
||||
|
||||
StdLogosResult result = module->get_cryptarchia_info();
|
||||
LOGOS_ASSERT_TRUE(contains(result.value.get<std::string>(), "NotStarted"));
|
||||
delete module;
|
||||
}
|
||||
|
||||
LOGOS_TEST(get_cryptarchia_info_bootstrapping_mode) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
TempDir tmpDir;
|
||||
@ -1191,6 +1392,147 @@ LOGOS_TEST(get_cryptarchia_info_returns_error_on_ffi_failure) {
|
||||
delete module;
|
||||
}
|
||||
|
||||
LOGOS_TEST(get_block_events_returns_json_on_success) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
TempDir tmpDir;
|
||||
auto* module = createStartedModule(t, tmpDir);
|
||||
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||
|
||||
t.mockCFunction("get_block_events").returns(R"([{"type":"inscription"}])");
|
||||
t.mockCFunction("get_block_events_error").returns(0);
|
||||
|
||||
StdLogosResult result = module->get_block_events(VALID_HEX);
|
||||
LOGOS_ASSERT_TRUE(result.success);
|
||||
LOGOS_ASSERT_TRUE(contains(result.value.get<std::string>(), "inscription"));
|
||||
LOGOS_ASSERT(t.cFunctionCalled("get_block_events"));
|
||||
LOGOS_ASSERT(t.cFunctionCalled("free_cstring"));
|
||||
delete module;
|
||||
}
|
||||
|
||||
LOGOS_TEST(get_block_events_returns_error_on_ffi_failure) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
TempDir tmpDir;
|
||||
auto* module = createStartedModule(t, tmpDir);
|
||||
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||
|
||||
t.mockCFunction("get_block_events_error").returns(1);
|
||||
|
||||
LOGOS_ASSERT_FALSE(module->get_block_events(VALID_HEX).success);
|
||||
delete module;
|
||||
}
|
||||
|
||||
// Time
|
||||
|
||||
LOGOS_TEST(get_time_info_returns_json_on_success) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
TempDir tmpDir;
|
||||
auto* module = createStartedModule(t, tmpDir);
|
||||
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||
|
||||
t.mockCFunction("get_time_info_error").returns(0);
|
||||
t.mockCFunction("time_slot_duration_ms").returns(2000);
|
||||
t.mockCFunction("time_genesis_time_unix_ms").returns(1700000);
|
||||
t.mockCFunction("time_current_slot").returns(1234);
|
||||
t.mockCFunction("time_current_epoch").returns(7);
|
||||
|
||||
StdLogosResult result = module->get_time_info();
|
||||
LOGOS_ASSERT_TRUE(result.success);
|
||||
std::string json = result.value.get<std::string>();
|
||||
LOGOS_ASSERT_TRUE(contains(json, "\"slot_duration_ms\":2000"));
|
||||
LOGOS_ASSERT_TRUE(contains(json, "\"genesis_time_unix_ms\":1700000"));
|
||||
LOGOS_ASSERT_TRUE(contains(json, "\"current_slot\":1234"));
|
||||
LOGOS_ASSERT_TRUE(contains(json, "\"current_epoch\":7"));
|
||||
LOGOS_ASSERT(t.cFunctionCalled("get_time_info"));
|
||||
LOGOS_ASSERT(t.cFunctionCalled("free_time_info"));
|
||||
delete module;
|
||||
}
|
||||
|
||||
LOGOS_TEST(get_time_info_returns_error_on_ffi_failure) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
TempDir tmpDir;
|
||||
auto* module = createStartedModule(t, tmpDir);
|
||||
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||
|
||||
t.mockCFunction("get_time_info_error").returns(1);
|
||||
|
||||
LOGOS_ASSERT_FALSE(module->get_time_info().success);
|
||||
delete module;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Stream events (trampolines drive logos_events; end of stream = JSON null)
|
||||
// ============================================================================
|
||||
|
||||
// Captured by the mock subscribe calls (mock_logos_blockchain.cpp).
|
||||
extern BlockCallback g_lastNewBlockCallback;
|
||||
extern BlockCallback g_lastProcessedBlockCallback;
|
||||
extern BlockCallback g_lastLibBlockCallback;
|
||||
// Recorded by the event stubs (event_stubs.cpp).
|
||||
extern std::string g_lastNewBlockEventJson;
|
||||
extern std::string g_lastProcessedBlockEventJson;
|
||||
extern std::string g_lastLibBlockEventJson;
|
||||
|
||||
LOGOS_TEST(processed_block_stream_forwards_json_and_null_sentinel) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
TempDir tmpDir;
|
||||
auto* module = createStartedModule(t, tmpDir);
|
||||
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||
LOGOS_ASSERT_TRUE(g_lastProcessedBlockCallback != nullptr);
|
||||
|
||||
g_lastProcessedBlockEventJson.clear();
|
||||
g_lastProcessedBlockCallback(R"({"header_id":"abc","transactions":[]})");
|
||||
LOGOS_ASSERT_EQ(g_lastProcessedBlockEventJson, std::string(R"({"header_id":"abc","transactions":[]})"));
|
||||
|
||||
g_lastProcessedBlockCallback(nullptr);
|
||||
LOGOS_ASSERT_EQ(g_lastProcessedBlockEventJson, std::string("null"));
|
||||
delete module;
|
||||
}
|
||||
|
||||
LOGOS_TEST(lib_block_stream_forwards_json_and_null_sentinel) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
TempDir tmpDir;
|
||||
auto* module = createStartedModule(t, tmpDir);
|
||||
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||
LOGOS_ASSERT_TRUE(g_lastLibBlockCallback != nullptr);
|
||||
|
||||
g_lastLibBlockEventJson.clear();
|
||||
g_lastLibBlockCallback(R"({"header_id":"def","slot":9})");
|
||||
LOGOS_ASSERT_EQ(g_lastLibBlockEventJson, std::string(R"({"header_id":"def","slot":9})"));
|
||||
|
||||
g_lastLibBlockCallback(nullptr);
|
||||
LOGOS_ASSERT_EQ(g_lastLibBlockEventJson, std::string("null"));
|
||||
delete module;
|
||||
}
|
||||
|
||||
// The legacy new-block stream never sends NULL, but the trampoline must not
|
||||
// crash if it ever does (regression test for the added guard).
|
||||
LOGOS_TEST(new_block_callback_ignores_null_pointer) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
TempDir tmpDir;
|
||||
auto* module = createStartedModule(t, tmpDir);
|
||||
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||
LOGOS_ASSERT_TRUE(g_lastNewBlockCallback != nullptr);
|
||||
|
||||
g_lastNewBlockEventJson.clear();
|
||||
g_lastNewBlockCallback(nullptr);
|
||||
LOGOS_ASSERT_EQ(g_lastNewBlockEventJson, std::string());
|
||||
delete module;
|
||||
}
|
||||
|
||||
// Stream events are not delivered after the module stops (s_instance cleared).
|
||||
LOGOS_TEST(stream_events_not_delivered_after_stop) {
|
||||
auto t = LogosTestContext("blockchain_module");
|
||||
TempDir tmpDir;
|
||||
auto* module = createStartedModule(t, tmpDir);
|
||||
LOGOS_ASSERT_TRUE(module != nullptr);
|
||||
LOGOS_ASSERT_TRUE(module->stop().success);
|
||||
|
||||
g_lastProcessedBlockEventJson.clear();
|
||||
g_lastProcessedBlockCallback(R"({"header_id":"abc"})");
|
||||
LOGOS_ASSERT_EQ(g_lastProcessedBlockEventJson, std::string());
|
||||
delete module;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Config management (operate on file paths, no running node required)
|
||||
// ============================================================================
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user