From 4828b081833acdd2455446331718fed0dff89f68 Mon Sep 17 00:00:00 2001 From: erhant Date: Wed, 17 Jun 2026 14:07:36 +0300 Subject: [PATCH 1/5] feat: add methods to indexer module to favor logos-protocol instead of RPC --- src/i_lez_indexer_module.h | 23 +++ src/lez_indexer_module.cpp | 413 +++++++++++++++++++++++++++++++++++++ src/lez_indexer_module.h | 11 + 3 files changed, 447 insertions(+) diff --git a/src/i_lez_indexer_module.h b/src/i_lez_indexer_module.h index eb716fc..f5887a3 100644 --- a/src/i_lez_indexer_module.h +++ b/src/i_lez_indexer_module.h @@ -21,6 +21,29 @@ public: const QString& config_path, const QString& port ) = 0; + + // Indexer Queries + // + // Every parameter is a QString for the same QtRO exact-type-match reason as + // `start_indexer` above: callers (consumer modules, the module-viewer) deliver + // arguments as QString, so numeric/hash params are passed as text and parsed + // internally. Each method returns a compact JSON string (the Logos protocol + // data model is JSON-in-strings); an empty string means "not found" or a + // failed query (the FFI does not log, so the body qWarns the status code). + // + // The JSON shape is tailored to what the LEZ explorer's Block/Transaction/ + // Account models consume (counts/sizes rather than raw arrays). Large 64-bit + // values (ids, timestamps, validity windows) are emitted as decimal STRINGS + // to avoid the double-precision loss of JSON numbers; counts/sizes are numbers. + virtual QString getAccount(const QString& account_id) = 0; + virtual QString getBlockById(const QString& block_id) = 0; + virtual QString getBlockByHash(const QString& hash) = 0; + virtual QString getTransaction(const QString& hash) = 0; + virtual QString getBlocks(const QString& before, const QString& limit) = 0; + virtual QString getLastFinalizedBlockId() = 0; + virtual QString getTransactionsByAccount(const QString& account_id, + const QString& offset, + const QString& limit) = 0; }; #define ILezIndexerModule_iid "org.logos.ilezindexermodule" diff --git a/src/lez_indexer_module.cpp b/src/lez_indexer_module.cpp index 1a5a33c..59b5255 100644 --- a/src/lez_indexer_module.cpp +++ b/src/lez_indexer_module.cpp @@ -1,12 +1,209 @@ #include "lez_indexer_module.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 + LezIndexerModule::LezIndexerModule() = default; LezIndexerModule::~LezIndexerModule() { @@ -66,3 +263,219 @@ int LezIndexerModule::start_indexer(const QString& config_path, const QString& p return 0; } + +// === Indexer Queries === +// +// Each method calls the matching query_* FFI function on the handle we already +// hold (no Runtime needed — the pinned FFI carries its own runtime inside the +// handle), marshals the returned C struct into compact JSON, then frees the +// FFI's deep allocations with the matching free_ffi_* function. +// +// Known leak: query_* returns its payload in a `Box::into_raw` *outer* box +// (PointerResult.value), and the free_ffi_* functions free only the inner +// boxes/vectors (they take the struct by value). The pinned FFI provides no +// free for the outer box, and libc free() would be UB if Rust's global +// allocator differs from the system one, so we deliberately leak the small +// (~8-32 byte) outer wrapper per query. Tracked for an upstream fix (add a +// free_*_result, or switch the query API to out-params). + +QString LezIndexerModule::getAccount(const QString& account_id) { + if (!indexer_service_ffi) { + qWarning() << "getAccount: indexer not started"; + return {}; + } + + FfiAccountId id; + if (!hexToBytes32(account_id, &id)) { + qWarning() << "getAccount: invalid account id (need 32-byte hex):" << account_id; + return {}; + } + + PointerResult_FfiAccount__OperationStatus res = ::query_account(indexer_service_ffi, id); + if (is_error(&res.error) || !res.value) { + qWarning() << "getAccount: indexer FFI error:" << res.error; + return {}; + } + + const QString out = jsonToCompactString(ffiAccountToJson(*res.value)); + ::free_ffi_account(*res.value); + return out; +} + +QString LezIndexerModule::getBlockById(const QString& block_id) { + if (!indexer_service_ffi) { + qWarning() << "getBlockById: indexer not started"; + return {}; + } + + bool ok = false; + const uint64_t id = block_id.toULongLong(&ok); + if (!ok) { + qWarning() << "getBlockById: invalid block id:" << block_id; + return {}; + } + + PointerResult_FfiBlockOpt__OperationStatus res = ::query_block(indexer_service_ffi, id); + if (is_error(&res.error) || !res.value) { + qWarning() << "getBlockById: indexer FFI error:" << res.error; + return {}; + } + + QString out; + if (res.value->is_some && res.value->value) { + out = jsonToCompactString(ffiBlockToJson(*res.value->value)); + } + ::free_ffi_block_opt(*res.value); + return out; +} + +QString LezIndexerModule::getBlockByHash(const QString& hash) { + if (!indexer_service_ffi) { + qWarning() << "getBlockByHash: indexer not started"; + return {}; + } + + FfiHashType h; + if (!hexToBytes32(hash, &h)) { + qWarning() << "getBlockByHash: invalid hash (need 32-byte hex):" << hash; + return {}; + } + + PointerResult_FfiBlockOpt__OperationStatus res = ::query_block_by_hash(indexer_service_ffi, h); + if (is_error(&res.error) || !res.value) { + qWarning() << "getBlockByHash: indexer FFI error:" << res.error; + return {}; + } + + QString out; + if (res.value->is_some && res.value->value) { + out = jsonToCompactString(ffiBlockToJson(*res.value->value)); + } + ::free_ffi_block_opt(*res.value); + return out; +} + +QString LezIndexerModule::getTransaction(const QString& hash) { + if (!indexer_service_ffi) { + qWarning() << "getTransaction: indexer not started"; + return {}; + } + + FfiHashType h; + if (!hexToBytes32(hash, &h)) { + qWarning() << "getTransaction: invalid hash (need 32-byte hex):" << hash; + return {}; + } + + PointerResult_FfiOption_FfiTransaction_____OperationStatus res = + ::query_transaction(indexer_service_ffi, h); + if (is_error(&res.error) || !res.value) { + qWarning() << "getTransaction: indexer FFI error:" << res.error; + return {}; + } + + QString out; + if (res.value->is_some && res.value->value) { + out = jsonToCompactString(ffiTransactionToJson(*res.value->value)); + } + ::free_ffi_transaction_opt(*res.value); + return out; +} + +QString LezIndexerModule::getBlocks(const QString& before, const QString& limit) { + if (!indexer_service_ffi) { + qWarning() << "getBlocks: indexer not started"; + return {}; + } + + bool limitOk = false; + const uint64_t limitNum = limit.toULongLong(&limitOk); + if (!limitOk) { + qWarning() << "getBlocks: invalid limit:" << limit; + return {}; + } + + // `before` is optional: an empty string means "from the tip". `beforeVal` + // must outlive the call since FfiOption_u64 borrows its address. + uint64_t beforeVal = 0; + bool hasBefore = false; + if (!before.isEmpty()) { + beforeVal = before.toULongLong(&hasBefore); + if (!hasBefore) { + qWarning() << "getBlocks: invalid before:" << before; + return {}; + } + } + FfiOption_u64 beforeOpt; + beforeOpt.is_some = hasBefore; + beforeOpt.value = hasBefore ? &beforeVal : nullptr; + + PointerResult_FfiVec_FfiBlock_____OperationStatus res = + ::query_block_vec(indexer_service_ffi, beforeOpt, limitNum); + if (is_error(&res.error) || !res.value) { + qWarning() << "getBlocks: indexer FFI error:" << res.error; + return {}; + } + + QJsonArray arr; + for (uintptr_t i = 0; i < res.value->len; ++i) { + arr.append(ffiBlockToJson(res.value->entries[i])); + } + ::free_ffi_block_vec(*res.value); + return jsonToCompactString(arr); +} + +QString LezIndexerModule::getLastFinalizedBlockId() { + if (!indexer_service_ffi) { + qWarning() << "getLastFinalizedBlockId: indexer not started"; + return {}; + } + + PointerResult_u64__OperationStatus res = ::query_last_block(indexer_service_ffi); + if (is_error(&res.error) || !res.value) { + qWarning() << "getLastFinalizedBlockId: indexer FFI error:" << res.error; + return {}; + } + + // Bare decimal string; the FFI provides no free for the boxed u64 (leaked, + // see the note above). + return u64ToString(*res.value); +} + +QString LezIndexerModule::getTransactionsByAccount(const QString& account_id, + const QString& offset, + const QString& limit) { + if (!indexer_service_ffi) { + qWarning() << "getTransactionsByAccount: indexer not started"; + return {}; + } + + FfiAccountId id; + if (!hexToBytes32(account_id, &id)) { + qWarning() << "getTransactionsByAccount: invalid account id (need 32-byte hex):" << account_id; + return {}; + } + + bool offsetOk = false; + bool limitOk = false; + const uint64_t offsetNum = offset.toULongLong(&offsetOk); + const uint64_t limitNum = limit.toULongLong(&limitOk); + if (!offsetOk || !limitOk) { + qWarning() << "getTransactionsByAccount: invalid offset/limit:" << offset << limit; + return {}; + } + + PointerResult_FfiVec_FfiTransaction_____OperationStatus res = + ::query_transactions_by_account(indexer_service_ffi, id, offsetNum, limitNum); + if (is_error(&res.error) || !res.value) { + qWarning() << "getTransactionsByAccount: indexer FFI error:" << res.error; + return {}; + } + + QJsonArray arr; + for (uintptr_t i = 0; i < res.value->len; ++i) { + arr.append(ffiTransactionToJson(res.value->entries[i])); + } + ::free_ffi_transaction_vec(*res.value); + return jsonToCompactString(arr); +} diff --git a/src/lez_indexer_module.h b/src/lez_indexer_module.h index 23dde79..8841c2c 100644 --- a/src/lez_indexer_module.h +++ b/src/lez_indexer_module.h @@ -41,6 +41,17 @@ public: // Indexer Lifecycle Q_INVOKABLE int start_indexer(const QString& config_path, const QString& port) override; + // Indexer Queries (return compact JSON strings; see i_lez_indexer_module.h) + Q_INVOKABLE QString getAccount(const QString& account_id) override; + Q_INVOKABLE QString getBlockById(const QString& block_id) override; + Q_INVOKABLE QString getBlockByHash(const QString& hash) override; + Q_INVOKABLE QString getTransaction(const QString& hash) override; + Q_INVOKABLE QString getBlocks(const QString& before, const QString& limit) override; + Q_INVOKABLE QString getLastFinalizedBlockId() override; + Q_INVOKABLE QString getTransactionsByAccount(const QString& account_id, + const QString& offset, + const QString& limit) override; + signals: void eventResponse(const QString& eventName, const QVariantList& data); }; From d309183bf870d3f4a04fd9500ebdcb2ca5cd2645 Mon Sep 17 00:00:00 2001 From: erhant Date: Wed, 17 Jun 2026 16:10:25 +0300 Subject: [PATCH 2/5] add commented `init_logger` --- src/lez_indexer_module.h | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/lez_indexer_module.h b/src/lez_indexer_module.h index 8841c2c..0d4d865 100644 --- a/src/lez_indexer_module.h +++ b/src/lez_indexer_module.h @@ -52,6 +52,19 @@ public: const QString& offset, const QString& limit) override; + // Indexer Logging (opt-in) + // + // Installs the indexer FFI's logger (env_logger) so the indexer's Rust `log` + // output surfaces in the host process. + // + // Commented out because the underlying `::init_logger()` export does not yet + // exist in the pinned logos-execution-zone FFI + // + // Uncomment this single block once the export lands upstream + // and the pin is bumped to a rev that includes it. + // + // Q_INVOKABLE void init_logger() { ::init_logger(); } + signals: void eventResponse(const QString& eventName, const QVariantList& data); }; From bae2daee2c0c8b08e551da2ee452834bb583c872 Mon Sep 17 00:00:00 2001 From: erhant Date: Wed, 17 Jun 2026 16:13:16 +0300 Subject: [PATCH 3/5] chore: `just prettify` --- src/i_lez_indexer_module.h | 13 ++- src/lez_indexer_module.cpp | 205 ++++++++++++++++++------------------- src/lez_indexer_module.h | 7 +- 3 files changed, 109 insertions(+), 116 deletions(-) diff --git a/src/i_lez_indexer_module.h b/src/i_lez_indexer_module.h index f5887a3..906754c 100644 --- a/src/i_lez_indexer_module.h +++ b/src/i_lez_indexer_module.h @@ -17,10 +17,7 @@ public: // ModuleProxy invokes by exact meta-type match and delivers caller arguments // as QString (the module-viewer reads every parameter as text); a uint16_t // parameter would never match. It is parsed to a port number internally. - virtual int start_indexer( - const QString& config_path, - const QString& port - ) = 0; + virtual int start_indexer(const QString& config_path, const QString& port) = 0; // Indexer Queries // @@ -41,9 +38,11 @@ public: virtual QString getTransaction(const QString& hash) = 0; virtual QString getBlocks(const QString& before, const QString& limit) = 0; virtual QString getLastFinalizedBlockId() = 0; - virtual QString getTransactionsByAccount(const QString& account_id, - const QString& offset, - const QString& limit) = 0; + virtual QString getTransactionsByAccount( + const QString& account_id, + const QString& offset, + const QString& limit + ) = 0; }; #define ILezIndexerModule_iid "org.logos.ilezindexermodule" diff --git a/src/lez_indexer_module.cpp b/src/lez_indexer_module.cpp index 59b5255..60c9352 100644 --- a/src/lez_indexer_module.cpp +++ b/src/lez_indexer_module.cpp @@ -1,7 +1,5 @@ #include "lez_indexer_module.h" -#include -#include #include #include #include @@ -9,82 +7,83 @@ #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. + // === 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()); -} + // 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]) { + // 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); + __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; + + // 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)); } - 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; -} + // 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 ffiTransactionToJson(const FfiTransaction& tx) { - QJsonObject obj; + 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; + } - switch (tx.kind) { + QJsonObject ffiTransactionToJson(const FfiTransaction& tx) { + QJsonObject obj; + + switch (tx.kind) { case Public: { const FfiPublicTransactionBody* body = tx.body.public_body; if (!body) { @@ -136,16 +135,12 @@ QJsonObject ffiTransactionToJson(const FfiTransaction& tx) { } 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("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("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; @@ -160,13 +155,13 @@ QJsonObject ffiTransactionToJson(const FfiTransaction& tx) { obj[QStringLiteral("bytecode_size")] = static_cast(body->message.len); break; } + } + + return obj; } - return obj; -} - -QString bedrockStatusToString(FfiBedrockStatus status) { - switch (status) { + QString bedrockStatusToString(FfiBedrockStatus status) { + switch (status) { case Safe: return QStringLiteral("Safe"); case Finalized: @@ -174,33 +169,33 @@ QString bedrockStatusToString(FfiBedrockStatus status) { 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); + 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])); + 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; } - obj[QStringLiteral("transactions")] = transactions; - return obj; -} -QString jsonToCompactString(const QJsonObject& obj) { - return QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact)); -} + 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)); -} + QString jsonToCompactString(const QJsonArray& arr) { + return QString::fromUtf8(QJsonDocument(arr).toJson(QJsonDocument::Compact)); + } } // namespace @@ -367,8 +362,7 @@ QString LezIndexerModule::getTransaction(const QString& hash) { return {}; } - PointerResult_FfiOption_FfiTransaction_____OperationStatus res = - ::query_transaction(indexer_service_ffi, h); + PointerResult_FfiOption_FfiTransaction_____OperationStatus res = ::query_transaction(indexer_service_ffi, h); if (is_error(&res.error) || !res.value) { qWarning() << "getTransaction: indexer FFI error:" << res.error; return {}; @@ -410,8 +404,7 @@ QString LezIndexerModule::getBlocks(const QString& before, const QString& limit) beforeOpt.is_some = hasBefore; beforeOpt.value = hasBefore ? &beforeVal : nullptr; - PointerResult_FfiVec_FfiBlock_____OperationStatus res = - ::query_block_vec(indexer_service_ffi, beforeOpt, limitNum); + PointerResult_FfiVec_FfiBlock_____OperationStatus res = ::query_block_vec(indexer_service_ffi, beforeOpt, limitNum); if (is_error(&res.error) || !res.value) { qWarning() << "getBlocks: indexer FFI error:" << res.error; return {}; @@ -442,9 +435,11 @@ QString LezIndexerModule::getLastFinalizedBlockId() { return u64ToString(*res.value); } -QString LezIndexerModule::getTransactionsByAccount(const QString& account_id, - const QString& offset, - const QString& limit) { +QString LezIndexerModule::getTransactionsByAccount( + const QString& account_id, + const QString& offset, + const QString& limit +) { if (!indexer_service_ffi) { qWarning() << "getTransactionsByAccount: indexer not started"; return {}; diff --git a/src/lez_indexer_module.h b/src/lez_indexer_module.h index 0d4d865..de9179d 100644 --- a/src/lez_indexer_module.h +++ b/src/lez_indexer_module.h @@ -48,9 +48,8 @@ public: Q_INVOKABLE QString getTransaction(const QString& hash) override; Q_INVOKABLE QString getBlocks(const QString& before, const QString& limit) override; Q_INVOKABLE QString getLastFinalizedBlockId() override; - Q_INVOKABLE QString getTransactionsByAccount(const QString& account_id, - const QString& offset, - const QString& limit) override; + Q_INVOKABLE QString + getTransactionsByAccount(const QString& account_id, const QString& offset, const QString& limit) override; // Indexer Logging (opt-in) // @@ -59,7 +58,7 @@ public: // // Commented out because the underlying `::init_logger()` export does not yet // exist in the pinned logos-execution-zone FFI - // + // // Uncomment this single block once the export lands upstream // and the pin is bumped to a rev that includes it. // From 953904db5b099a94f1bd85657cb1588361a9aa02 Mon Sep 17 00:00:00 2001 From: erhant Date: Wed, 17 Jun 2026 18:05:14 +0300 Subject: [PATCH 4/5] chore: docstrings for optional `before` --- src/i_lez_indexer_module.h | 3 +++ src/lez_indexer_module.cpp | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/i_lez_indexer_module.h b/src/i_lez_indexer_module.h index 906754c..8e66025 100644 --- a/src/i_lez_indexer_module.h +++ b/src/i_lez_indexer_module.h @@ -36,8 +36,11 @@ public: virtual QString getBlockById(const QString& block_id) = 0; virtual QString getBlockByHash(const QString& hash) = 0; virtual QString getTransaction(const QString& hash) = 0; + // `before` is the optional pagination cursor: a block id to page back from, + // or an empty string to start from the tip. `limit` caps the result count. virtual QString getBlocks(const QString& before, const QString& limit) = 0; virtual QString getLastFinalizedBlockId() = 0; + // `offset` and `limit` are both required (the paging window). virtual QString getTransactionsByAccount( const QString& account_id, const QString& offset, diff --git a/src/lez_indexer_module.cpp b/src/lez_indexer_module.cpp index 60c9352..24e299b 100644 --- a/src/lez_indexer_module.cpp +++ b/src/lez_indexer_module.cpp @@ -389,7 +389,8 @@ QString LezIndexerModule::getBlocks(const QString& before, const QString& limit) return {}; } - // `before` is optional: an empty string means "from the tip". `beforeVal` + // `before` is the optional pagination cursor: an empty string means "from + // the tip", a non-empty value that fails to parse is an error. `beforeVal` // must outlive the call since FfiOption_u64 borrows its address. uint64_t beforeVal = 0; bool hasBefore = false; From c789b7a6db1ad19c7d162dc8c92a7de7031eac30 Mon Sep 17 00:00:00 2001 From: erhant Date: Wed, 17 Jun 2026 18:22:41 +0300 Subject: [PATCH 5/5] 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