From c789b7a6db1ad19c7d162dc8c92a7de7031eac30 Mon Sep 17 00:00:00 2001 From: erhant Date: Wed, 17 Jun 2026 18:22:41 +0300 Subject: [PATCH] chore: group marshalling helpers to elsewhere --- CMakeLists.txt | 1 + src/lez_ffi_marshalling.cpp | 190 +++++++++++++++++++++++++++++++++++ src/lez_ffi_marshalling.h | 48 +++++++++ src/lez_indexer_ffi.h | 14 +++ src/lez_indexer_module.cpp | 195 +----------------------------------- src/lez_indexer_module.h | 9 +- 6 files changed, 257 insertions(+), 200 deletions(-) create mode 100644 src/lez_ffi_marshalling.cpp create mode 100644 src/lez_ffi_marshalling.h create mode 100644 src/lez_indexer_ffi.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 4b609f1..9ef6cf1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -177,6 +177,7 @@ set_target_properties(${PLUGIN_TARGET} PROPERTIES target_sources(${PLUGIN_TARGET} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/lez_indexer_module.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/lez_ffi_marshalling.cpp ) set_property(TARGET ${PLUGIN_TARGET} PROPERTY PUBLIC_HEADER diff --git a/src/lez_ffi_marshalling.cpp b/src/lez_ffi_marshalling.cpp new file mode 100644 index 0000000..ef28fe6 --- /dev/null +++ b/src/lez_ffi_marshalling.cpp @@ -0,0 +1,190 @@ +#include "lez_ffi_marshalling.h" + +#include +#include +#include +#include + +namespace marshalling { + + // Lower-case hex of `length` raw bytes (used for 32-byte hashes/ids/keys and + // 64-byte signatures). Qt's toHex() avoids a hand-rolled nibble loop. + QString bytesToHex(const uint8_t* data, const size_t length) { + return QString::fromLatin1(QByteArray(reinterpret_cast(data), static_cast(length)).toHex()); + } + + // FfiU128 is a 16-byte little-endian integer (balances, nonces). C++ has no + // native u128, so build the decimal string via __uint128_t (GCC/Clang, 64-bit). + QString u128LeToDecimal(const uint8_t data[16]) { +#if defined(__SIZEOF_INT128__) && __SIZEOF_INT128__ >= 16 + __uint128_t v = 0; + for (int i = 0; i < 16; ++i) { + v |= static_cast<__uint128_t>(data[i]) << (i * 8); + } + if (v == 0) { + return QStringLiteral("0"); + } + char buf[40]; + int n = 0; + while (v) { + buf[n++] = static_cast('0' + static_cast(v % 10)); + v /= 10; + } + std::reverse(buf, buf + n); + return QString::fromLatin1(buf, n); +#else +#error "u128LeToDecimal requires __uint128_t; build with GCC or Clang on 64-bit" +#endif + } + + // 64-bit values are emitted as decimal STRINGS, not JSON numbers: QJsonValue + // stores numbers as double, which silently loses precision above 2^53. + QString u64ToString(uint64_t v) { + return QString::number(static_cast(v)); + } + + // Parse a hex string (optionally 0x-prefixed) into a fixed 32-byte FfiBytes32. + // Returns false unless it decodes to exactly 32 bytes. + bool hexToBytes32(const QString& hex, FfiBytes32* out) { + QString trimmed = hex.trimmed(); + if (trimmed.startsWith(QLatin1String("0x")) || trimmed.startsWith(QLatin1String("0X"))) { + trimmed = trimmed.mid(2); + } + const QByteArray raw = QByteArray::fromHex(trimmed.toLatin1()); + if (raw.size() != 32) { + return false; + } + std::memcpy(out->data, raw.constData(), 32); + return true; + } + + QJsonObject ffiAccountToJson(const FfiAccount& account) { + QJsonObject obj; + obj[QStringLiteral("program_owner")] = + bytesToHex(reinterpret_cast(account.program_owner.data), 32); + obj[QStringLiteral("balance")] = u128LeToDecimal(account.balance.data); + obj[QStringLiteral("nonce")] = u128LeToDecimal(account.nonce.data); + obj[QStringLiteral("data_size")] = static_cast(account.data_len); + return obj; + } + + QJsonObject ffiTransactionToJson(const FfiTransaction& tx) { + QJsonObject obj; + + switch (tx.kind) { + case Public: { + const FfiPublicTransactionBody* body = tx.body.public_body; + if (!body) { + break; + } + obj[QStringLiteral("type")] = QStringLiteral("Public"); + obj[QStringLiteral("hash")] = bytesToHex(body->hash.data, 32); + obj[QStringLiteral("program_id")] = + bytesToHex(reinterpret_cast(body->message.program_id.data), 32); + + QJsonArray accounts; + const FfiAccountIdList& ids = body->message.account_ids; + const FfiNonceList& nonces = body->message.nonces; + for (uintptr_t i = 0; i < ids.len; ++i) { + QJsonObject ref; + ref[QStringLiteral("account_id")] = bytesToHex(ids.entries[i].data, 32); + ref[QStringLiteral("nonce")] = + i < nonces.len ? u128LeToDecimal(nonces.entries[i].data) : QStringLiteral("0"); + accounts.append(ref); + } + obj[QStringLiteral("accounts")] = accounts; + + QJsonArray instructionData; + const FfiInstructionDataList& instr = body->message.instruction_data; + for (uintptr_t i = 0; i < instr.len; ++i) { + instructionData.append(static_cast(instr.entries[i])); + } + obj[QStringLiteral("instruction_data")] = instructionData; + obj[QStringLiteral("signature_count")] = static_cast(body->witness_set.len); + break; + } + case Private: { + const FfiPrivateTransactionBody* body = tx.body.private_body; + if (!body) { + break; + } + obj[QStringLiteral("type")] = QStringLiteral("PrivacyPreserving"); + obj[QStringLiteral("hash")] = bytesToHex(body->hash.data, 32); + + QJsonArray accounts; + const FfiAccountIdList& ids = body->message.public_account_ids; + const FfiNonceList& nonces = body->message.nonces; + for (uintptr_t i = 0; i < ids.len; ++i) { + QJsonObject ref; + ref[QStringLiteral("account_id")] = bytesToHex(ids.entries[i].data, 32); + ref[QStringLiteral("nonce")] = + i < nonces.len ? u128LeToDecimal(nonces.entries[i].data) : QStringLiteral("0"); + accounts.append(ref); + } + obj[QStringLiteral("accounts")] = accounts; + + obj[QStringLiteral("new_commitments_count")] = static_cast(body->message.new_commitments.len); + obj[QStringLiteral("nullifiers_count")] = static_cast(body->message.new_nullifiers.len); + obj[QStringLiteral("encrypted_states_count")] = + static_cast(body->message.encrypted_private_post_states.len); + obj[QStringLiteral("validity_window_start")] = u64ToString(body->message.block_validity_window[0]); + obj[QStringLiteral("validity_window_end")] = u64ToString(body->message.block_validity_window[1]); + obj[QStringLiteral("signature_count")] = static_cast(body->witness_set.len); + obj[QStringLiteral("proof_size")] = static_cast(body->proof.len); + break; + } + case ProgramDeploy: { + const FfiProgramDeploymentTransactionBody* body = tx.body.program_deployment_body; + if (!body) { + break; + } + obj[QStringLiteral("type")] = QStringLiteral("ProgramDeployment"); + obj[QStringLiteral("hash")] = bytesToHex(body->hash.data, 32); + obj[QStringLiteral("bytecode_size")] = static_cast(body->message.len); + break; + } + } + + return obj; + } + + namespace { + QString bedrockStatusToString(FfiBedrockStatus status) { + switch (status) { + case Safe: + return QStringLiteral("Safe"); + case Finalized: + return QStringLiteral("Finalized"); + case Pending: + default: + return QStringLiteral("Pending"); + } + } + } // namespace + + QJsonObject ffiBlockToJson(const FfiBlock& block) { + QJsonObject obj; + obj[QStringLiteral("block_id")] = u64ToString(block.header.block_id); + obj[QStringLiteral("hash")] = bytesToHex(block.header.hash.data, 32); + obj[QStringLiteral("prev_block_hash")] = bytesToHex(block.header.prev_block_hash.data, 32); + obj[QStringLiteral("timestamp")] = u64ToString(block.header.timestamp); + obj[QStringLiteral("signature")] = bytesToHex(block.header.signature.data, 64); + obj[QStringLiteral("bedrock_status")] = bedrockStatusToString(block.bedrock_status); + + QJsonArray transactions; + for (uintptr_t i = 0; i < block.body.len; ++i) { + transactions.append(ffiTransactionToJson(block.body.entries[i])); + } + obj[QStringLiteral("transactions")] = transactions; + return obj; + } + + QString jsonToCompactString(const QJsonObject& obj) { + return QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact)); + } + + QString jsonToCompactString(const QJsonArray& arr) { + return QString::fromUtf8(QJsonDocument(arr).toJson(QJsonDocument::Compact)); + } + +} // namespace marshalling diff --git a/src/lez_ffi_marshalling.h b/src/lez_ffi_marshalling.h new file mode 100644 index 0000000..53b76ac --- /dev/null +++ b/src/lez_ffi_marshalling.h @@ -0,0 +1,48 @@ +#pragma once + +#include "lez_indexer_ffi.h" + +#include +#include +#include + +// FFI marshalling helpers +// +// Bridge the raw C structs returned by the indexer FFI (indexer_ffi.h) to the +// compact JSON the LEZ explorer's Block/Transaction/Account models consume, and +// parse the QString inputs back into FFI types. Kept in their own translation +// unit so lez_indexer_module.cpp stays focused on the query flow and the JSON +// data model lives in one place — which is also what makes it easy to share +// with other modules later. +// +// These never take ownership of FFI memory: the caller frees it with the +// matching free_ffi_* function AFTER marshalling. +// +// Large 64-bit values (ids, timestamps, validity windows) and 128-bit values +// (balances, nonces) are emitted as decimal STRINGS to avoid the double- +// precision loss of JSON numbers; counts/sizes are emitted as numbers. +namespace marshalling { + + // Lower-case hex of `length` raw bytes (32-byte hashes/ids/keys, 64-byte + // signatures). + QString bytesToHex(const uint8_t* data, size_t length); + + // FfiU128 is a 16-byte little-endian integer (balances, nonces); rendered + // as a decimal string. + QString u128LeToDecimal(const uint8_t data[16]); + + // 64-bit value as a decimal string. + QString u64ToString(uint64_t v); + + // Parse a hex string (optionally 0x-prefixed) into a fixed 32-byte buffer. + // Returns false unless it decodes to exactly 32 bytes. + bool hexToBytes32(const QString& hex, FfiBytes32* out); + + QJsonObject ffiAccountToJson(const FfiAccount& account); + QJsonObject ffiTransactionToJson(const FfiTransaction& tx); + QJsonObject ffiBlockToJson(const FfiBlock& block); + + QString jsonToCompactString(const QJsonObject& obj); + QString jsonToCompactString(const QJsonArray& arr); + +} // namespace marshalling diff --git a/src/lez_indexer_ffi.h b/src/lez_indexer_ffi.h new file mode 100644 index 0000000..ee3b247 --- /dev/null +++ b/src/lez_indexer_ffi.h @@ -0,0 +1,14 @@ +#pragma once + +// The generated indexer_ffi.h carries no include guard, so including it from +// more than one header in the same translation unit redefines every type. +// Wrap it once here (behind #pragma once) and include THIS header instead of +// directly, so the FFI types can be shared by both +// lez_indexer_module.h and lez_ffi_marshalling.h. +#ifdef __cplusplus +extern "C" { +#endif +#include +#ifdef __cplusplus +} +#endif diff --git a/src/lez_indexer_module.cpp b/src/lez_indexer_module.cpp index 24e299b..13a5616 100644 --- a/src/lez_indexer_module.cpp +++ b/src/lez_indexer_module.cpp @@ -1,203 +1,14 @@ #include "lez_indexer_module.h" +#include "lez_ffi_marshalling.h" + #include #include #include -#include -#include #include #include -#include -#include -namespace { - - // === FFI marshalling helpers === - // - // These turn the raw C structs returned by the indexer FFI into the compact - // JSON described in i_lez_indexer_module.h. They never take ownership of FFI - // memory — the caller frees it with the matching free_ffi_* function AFTER - // marshalling. - - // Lower-case hex of `length` raw bytes (used for 32-byte hashes/ids/keys and - // 64-byte signatures). Qt's toHex() avoids a hand-rolled nibble loop. - QString bytesToHex(const uint8_t* data, const size_t length) { - return QString::fromLatin1(QByteArray(reinterpret_cast(data), static_cast(length)).toHex()); - } - - // FfiU128 is a 16-byte little-endian integer (balances, nonces). C++ has no - // native u128, so build the decimal string via __uint128_t (GCC/Clang, 64-bit). - QString u128LeToDecimal(const uint8_t data[16]) { -#if defined(__SIZEOF_INT128__) && __SIZEOF_INT128__ >= 16 - __uint128_t v = 0; - for (int i = 0; i < 16; ++i) { - v |= static_cast<__uint128_t>(data[i]) << (i * 8); - } - if (v == 0) { - return QStringLiteral("0"); - } - char buf[40]; - int n = 0; - while (v) { - buf[n++] = static_cast('0' + static_cast(v % 10)); - v /= 10; - } - std::reverse(buf, buf + n); - return QString::fromLatin1(buf, n); -#else -#error "u128LeToDecimal requires __uint128_t; build with GCC or Clang on 64-bit" -#endif - } - - // 64-bit values are emitted as decimal STRINGS, not JSON numbers: QJsonValue - // stores numbers as double, which silently loses precision above 2^53. - QString u64ToString(uint64_t v) { - return QString::number(static_cast(v)); - } - - // Parse a hex string (optionally 0x-prefixed) into a fixed 32-byte FfiBytes32. - // Returns false unless it decodes to exactly 32 bytes. - bool hexToBytes32(const QString& hex, FfiBytes32* out) { - QString trimmed = hex.trimmed(); - if (trimmed.startsWith(QLatin1String("0x")) || trimmed.startsWith(QLatin1String("0X"))) { - trimmed = trimmed.mid(2); - } - const QByteArray raw = QByteArray::fromHex(trimmed.toLatin1()); - if (raw.size() != 32) { - return false; - } - std::memcpy(out->data, raw.constData(), 32); - return true; - } - - QJsonObject ffiAccountToJson(const FfiAccount& account) { - QJsonObject obj; - obj[QStringLiteral("program_owner")] = - bytesToHex(reinterpret_cast(account.program_owner.data), 32); - obj[QStringLiteral("balance")] = u128LeToDecimal(account.balance.data); - obj[QStringLiteral("nonce")] = u128LeToDecimal(account.nonce.data); - obj[QStringLiteral("data_size")] = static_cast(account.data_len); - return obj; - } - - QJsonObject ffiTransactionToJson(const FfiTransaction& tx) { - QJsonObject obj; - - switch (tx.kind) { - case Public: { - const FfiPublicTransactionBody* body = tx.body.public_body; - if (!body) { - break; - } - obj[QStringLiteral("type")] = QStringLiteral("Public"); - obj[QStringLiteral("hash")] = bytesToHex(body->hash.data, 32); - obj[QStringLiteral("program_id")] = - bytesToHex(reinterpret_cast(body->message.program_id.data), 32); - - QJsonArray accounts; - const FfiAccountIdList& ids = body->message.account_ids; - const FfiNonceList& nonces = body->message.nonces; - for (uintptr_t i = 0; i < ids.len; ++i) { - QJsonObject ref; - ref[QStringLiteral("account_id")] = bytesToHex(ids.entries[i].data, 32); - ref[QStringLiteral("nonce")] = - i < nonces.len ? u128LeToDecimal(nonces.entries[i].data) : QStringLiteral("0"); - accounts.append(ref); - } - obj[QStringLiteral("accounts")] = accounts; - - QJsonArray instructionData; - const FfiInstructionDataList& instr = body->message.instruction_data; - for (uintptr_t i = 0; i < instr.len; ++i) { - instructionData.append(static_cast(instr.entries[i])); - } - obj[QStringLiteral("instruction_data")] = instructionData; - obj[QStringLiteral("signature_count")] = static_cast(body->witness_set.len); - break; - } - case Private: { - const FfiPrivateTransactionBody* body = tx.body.private_body; - if (!body) { - break; - } - obj[QStringLiteral("type")] = QStringLiteral("PrivacyPreserving"); - obj[QStringLiteral("hash")] = bytesToHex(body->hash.data, 32); - - QJsonArray accounts; - const FfiAccountIdList& ids = body->message.public_account_ids; - const FfiNonceList& nonces = body->message.nonces; - for (uintptr_t i = 0; i < ids.len; ++i) { - QJsonObject ref; - ref[QStringLiteral("account_id")] = bytesToHex(ids.entries[i].data, 32); - ref[QStringLiteral("nonce")] = - i < nonces.len ? u128LeToDecimal(nonces.entries[i].data) : QStringLiteral("0"); - accounts.append(ref); - } - obj[QStringLiteral("accounts")] = accounts; - - obj[QStringLiteral("new_commitments_count")] = static_cast(body->message.new_commitments.len); - obj[QStringLiteral("nullifiers_count")] = static_cast(body->message.new_nullifiers.len); - obj[QStringLiteral("encrypted_states_count")] = - static_cast(body->message.encrypted_private_post_states.len); - obj[QStringLiteral("validity_window_start")] = u64ToString(body->message.block_validity_window[0]); - obj[QStringLiteral("validity_window_end")] = u64ToString(body->message.block_validity_window[1]); - obj[QStringLiteral("signature_count")] = static_cast(body->witness_set.len); - obj[QStringLiteral("proof_size")] = static_cast(body->proof.len); - break; - } - case ProgramDeploy: { - const FfiProgramDeploymentTransactionBody* body = tx.body.program_deployment_body; - if (!body) { - break; - } - obj[QStringLiteral("type")] = QStringLiteral("ProgramDeployment"); - obj[QStringLiteral("hash")] = bytesToHex(body->hash.data, 32); - obj[QStringLiteral("bytecode_size")] = static_cast(body->message.len); - break; - } - } - - return obj; - } - - QString bedrockStatusToString(FfiBedrockStatus status) { - switch (status) { - case Safe: - return QStringLiteral("Safe"); - case Finalized: - return QStringLiteral("Finalized"); - case Pending: - default: - return QStringLiteral("Pending"); - } - } - - QJsonObject ffiBlockToJson(const FfiBlock& block) { - QJsonObject obj; - obj[QStringLiteral("block_id")] = u64ToString(block.header.block_id); - obj[QStringLiteral("hash")] = bytesToHex(block.header.hash.data, 32); - obj[QStringLiteral("prev_block_hash")] = bytesToHex(block.header.prev_block_hash.data, 32); - obj[QStringLiteral("timestamp")] = u64ToString(block.header.timestamp); - obj[QStringLiteral("signature")] = bytesToHex(block.header.signature.data, 64); - obj[QStringLiteral("bedrock_status")] = bedrockStatusToString(block.bedrock_status); - - QJsonArray transactions; - for (uintptr_t i = 0; i < block.body.len; ++i) { - transactions.append(ffiTransactionToJson(block.body.entries[i])); - } - obj[QStringLiteral("transactions")] = transactions; - return obj; - } - - QString jsonToCompactString(const QJsonObject& obj) { - return QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact)); - } - - QString jsonToCompactString(const QJsonArray& arr) { - return QString::fromUtf8(QJsonDocument(arr).toJson(QJsonDocument::Compact)); - } - -} // namespace +using namespace marshalling; LezIndexerModule::LezIndexerModule() = default; diff --git a/src/lez_indexer_module.h b/src/lez_indexer_module.h index de9179d..3fcb228 100644 --- a/src/lez_indexer_module.h +++ b/src/lez_indexer_module.h @@ -1,14 +1,7 @@ #pragma once #include "i_lez_indexer_module.h" - -#ifdef __cplusplus -extern "C" { -#endif -#include -#ifdef __cplusplus -} -#endif +#include "lez_indexer_ffi.h" #include #include