From b11254e027e57f72f7b03956d7cc93e75cc71f35 Mon Sep 17 00:00:00 2001 From: Artem Gureev Date: Tue, 11 Aug 2026 18:15:57 +0000 Subject: [PATCH] feat(indexer): expose events over FFI --- lez/indexer/ffi/indexer_ffi.h | 102 ++++++++++++++++++ lez/indexer/ffi/src/api/query.rs | 135 +++++++++++++++++++++++- lez/indexer/ffi/src/api/types/event.rs | 68 ++++++++++++ lez/indexer/ffi/src/api/types/mod.rs | 17 ++- lez/indexer/ffi/src/errors.rs | 1 + lez/indexer/ffi/src/lib.rs | 9 ++ lez/indexer/service/protocol/src/lib.rs | 3 +- 7 files changed, 330 insertions(+), 5 deletions(-) create mode 100644 lez/indexer/ffi/src/api/types/event.rs diff --git a/lez/indexer/ffi/indexer_ffi.h b/lez/indexer/ffi/indexer_ffi.h index 561c5e5f2..ff833a196 100644 --- a/lez/indexer/ffi/indexer_ffi.h +++ b/lez/indexer/ffi/indexer_ffi.h @@ -5,11 +5,17 @@ #include #include +/** + * Largest block span a single `query_events` range request may cover. + */ +#define MAX_EVENT_QUERY_BLOCK_SPAN 1000 + typedef enum OperationStatus { Ok = 0, NullPointer = 1, InitializationError = 2, ClientError = 3, + InvalidArgument = 4, } OperationStatus; typedef enum FfiTransactionKind { @@ -394,6 +400,41 @@ typedef struct PointerResult_FfiVec_FfiTransaction_____OperationStatus { enum OperationStatus error; } PointerResult_FfiVec_FfiTransaction_____OperationStatus; +/** + * 8-byte array type for event selectors. + */ +typedef struct FfiBytes8 { + uint8_t data[8]; +} FfiBytes8; + +typedef struct FfiBytes8 FfiSelector; + +typedef struct FfiEventRecord { + FfiBlockId block_id; + uint32_t tx_index; + FfiHashType tx_hash; + struct FfiProgramId program_id; + FfiSelector selector; + FfiVecU8 data; +} FfiEventRecord; + +typedef struct FfiVec_FfiEventRecord { + struct FfiEventRecord *entries; + uintptr_t len; + uintptr_t capacity; +} FfiVec_FfiEventRecord; + +/** + * Simple wrapper around a pointer to a value or an error. + * + * Pointer is not guaranteed. You should check the error field before + * dereferencing the pointer. + */ +typedef struct PointerResult_FfiVec_FfiEventRecord_____OperationStatus { + struct FfiVec_FfiEventRecord *value; + enum OperationStatus error; +} PointerResult_FfiVec_FfiEventRecord_____OperationStatus; + #ifdef __cplusplus extern "C" { #endif // __cplusplus @@ -639,6 +680,45 @@ struct PointerResult_FfiVec_FfiTransaction_____OperationStatus query_transaction uint64_t offset, uint64_t limit); +/** + * Query events emitted by programs, optionally filtered. + * + * Resolution mirrors the `getEvents` RPC: a non-null `tx_hash` makes this a point + * lookup and the block range is ignored; otherwise the range from `from_block` to + * `to_block` (defaulting to the current tip when none) is read, capped at + * `MAX_EVENT_QUERY_BLOCK_SPAN` blocks — `InvalidArgument` when exceeded. `program_id` + * and `selector` are exact-match filters applied to the result. + * + * # Arguments + * + * - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried. + * - `from_block`: Inclusive range start, ignored when `tx_hash` is non-null. + * - `to_block`: `FfiOption` - inclusive range end; none means the current tip. Ignored when + * `tx_hash` is non-null. + * - `tx_hash`: Optional transaction hash; null means absent. + * - `program_id`: Optional emitting-program filter; null means absent. + * - `selector`: Optional event-selector filter; null means absent. + * + * # Returns + * + * A [`PointerResult`] holding an `FfiVec` that the caller MUST free + * with `free_ffi_event_record_vec`, or an error status. + * + * # Safety + * + * The caller must ensure that: + * - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. + * - if `to_block.is_some`, its `value` points to a valid `u64`. + * - each of `tx_hash`, `program_id` and `selector` is either null or a valid pointer to its + * respective type. + */ +struct PointerResult_FfiVec_FfiEventRecord_____OperationStatus query_events(const struct IndexerServiceFFI *indexer, + uint64_t from_block, + struct FfiOption_u64 to_block, + const FfiHashType *tx_hash, + const struct FfiProgramId *program_id, + const FfiSelector *selector); + /** * Frees the resources associated with the given ffi account. * @@ -730,6 +810,28 @@ void free_ffi_block_opt(FfiBlockOpt *val); */ void free_ffi_block_vec(struct FfiVec_FfiBlock *val); +/** + * Frees the resources associated with the given vector of ffi event records. + * + * Takes ownership of the whole allocation produced by `query_events`: the outer + * `Box>` (the `PointerResult.value` pointer), the vector's + * backing buffer, and every record's payload within it. + * + * # Arguments + * + * - `val`: The `*mut FfiVec` returned in `PointerResult.value`. + * + * # Returns + * + * void. + * + * # Safety + * + * The caller must ensure that: + * - `val` is a pointer to an `FfiVec` produced by this library and not yet freed. + */ +void free_ffi_event_record_vec(struct FfiVec_FfiEventRecord *val); + /** * Frees the resources associated with the given ffi transaction. * diff --git a/lez/indexer/ffi/src/api/query.rs b/lez/indexer/ffi/src/api/query.rs index 9f0f61815..319b2de86 100644 --- a/lez/indexer/ffi/src/api/query.rs +++ b/lez/indexer/ffi/src/api/query.rs @@ -1,15 +1,16 @@ use std::ffi::{CString, c_char}; -use indexer_service_protocol::AccountId; +use indexer_service_protocol::{AccountId, EventRecord}; use crate::{ - IndexerServiceFFI, + IndexerServiceFFI, MAX_EVENT_QUERY_BLOCK_SPAN, api::{ PointerResult, types::{ - FfiAccountId, FfiBlockId, FfiHashType, FfiOption, FfiVec, + FfiAccountId, FfiBlockId, FfiHashType, FfiOption, FfiProgramId, FfiSelector, FfiVec, account::FfiAccount, block::{FfiBlock, FfiBlockOpt}, + event::FfiEventRecord, transaction::FfiTransaction, }, }, @@ -434,3 +435,131 @@ pub unsafe extern "C" fn query_transactions_by_account( }, ) } + +/// Query events emitted by programs, optionally filtered. +/// +/// Resolution mirrors the `getEvents` RPC: a non-null `tx_hash` makes this a point +/// lookup and the block range is ignored; otherwise the range from `from_block` to +/// `to_block` (defaulting to the current tip when none) is read, capped at +/// `MAX_EVENT_QUERY_BLOCK_SPAN` blocks — `InvalidArgument` when exceeded. `program_id` +/// and `selector` are exact-match filters applied to the result. +/// +/// # Arguments +/// +/// - `indexer`: A pointer to the [`IndexerServiceFFI`] instance to be queried. +/// - `from_block`: Inclusive range start, ignored when `tx_hash` is non-null. +/// - `to_block`: `FfiOption` - inclusive range end; none means the current tip. Ignored when +/// `tx_hash` is non-null. +/// - `tx_hash`: Optional transaction hash; null means absent. +/// - `program_id`: Optional emitting-program filter; null means absent. +/// - `selector`: Optional event-selector filter; null means absent. +/// +/// # Returns +/// +/// A [`PointerResult`] holding an `FfiVec` that the caller MUST free +/// with `free_ffi_event_record_vec`, or an error status. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `indexer` is a valid pointer to a [`IndexerServiceFFI`] instance. +/// - if `to_block.is_some`, its `value` points to a valid `u64`. +/// - each of `tx_hash`, `program_id` and `selector` is either null or a valid pointer to its +/// respective type. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn query_events( + indexer: *const IndexerServiceFFI, + from_block: u64, + to_block: FfiOption, + tx_hash: *const FfiHashType, + program_id: *const FfiProgramId, + selector: *const FfiSelector, +) -> PointerResult, OperationStatus> { + if indexer.is_null() { + log::error!("Attempted to query a null indexer pointer. This is a bug. Aborting."); + return PointerResult::from_error(OperationStatus::NullPointer); + } + + let indexer = unsafe { &*indexer }; + let program_id = + unsafe { program_id.as_ref() }.map(|id| indexer_service_protocol::ProgramId(id.data)); + let selector = unsafe { selector.as_ref() }.map(|s| indexer_service_protocol::Selector(s.data)); + + let records = if let Some(tx_hash) = unsafe { tx_hash.as_ref() } { + indexer + .core() + .store + .block_id_by_tx_hash(tx_hash.data) + .and_then(|resolved| { + resolved.map_or(Ok(Vec::new()), |block_id| { + indexer + .core() + .store + .get_events_for_block(block_id) + .map(|row| { + row.and_then(|groups| { + groups + .into_iter() + .find(|group| group.tx_hash.0 == tx_hash.data) + }) + .map(|group| EventRecord::from_tx_events(block_id, group)) + .unwrap_or_default() + }) + }) + }) + } else { + if to_block.is_some && to_block.value.is_null() { + log::error!("query_events to_block is flagged present but its value pointer is null"); + return PointerResult::from_error(OperationStatus::InvalidArgument); + } + let to_block = match to_block.is_some.then(|| unsafe { *to_block.value }) { + Some(to_block) => to_block, + None => match indexer.core().store.get_last_block_id() { + Ok(tip) => tip.unwrap_or(0), + Err(e) => { + log::error!("Failed to query events: {e:#}"); + return PointerResult::from_error(OperationStatus::ClientError); + } + }, + }; + // Same bound as the RPC surface, from the shared constant. + let span = to_block.saturating_sub(from_block).saturating_add(1); + if span > MAX_EVENT_QUERY_BLOCK_SPAN { + log::error!( + "query_events block span {span} exceeds the maximum of {MAX_EVENT_QUERY_BLOCK_SPAN}" + ); + return PointerResult::from_error(OperationStatus::InvalidArgument); + } + indexer + .core() + .store + .get_events_range(from_block, to_block) + .map(|groups| { + groups + .into_iter() + .flat_map(|(block_id, groups)| { + groups + .into_iter() + .flat_map(move |group| EventRecord::from_tx_events(block_id, group)) + }) + .collect::>() + }) + }; + + records.map_or_else( + |e| { + log::error!("Failed to query events: {e:#}"); + PointerResult::from_error(OperationStatus::ClientError) + }, + |records| { + PointerResult::from_value( + records + .into_iter() + .filter(|record| record.matches_fields(program_id, selector)) + .map(Into::into) + .collect::>() + .into(), + ) + }, + ) +} diff --git a/lez/indexer/ffi/src/api/types/event.rs b/lez/indexer/ffi/src/api/types/event.rs new file mode 100644 index 000000000..72157608c --- /dev/null +++ b/lez/indexer/ffi/src/api/types/event.rs @@ -0,0 +1,68 @@ +use indexer_service_protocol::EventRecord; + +use crate::api::types::{ + FfiBlockId, FfiHashType, FfiProgramId, FfiSelector, FfiVec, vectors::FfiVecU8, +}; + +#[repr(C)] +pub struct FfiEventRecord { + pub block_id: FfiBlockId, + pub tx_index: u32, + pub tx_hash: FfiHashType, + pub program_id: FfiProgramId, + pub selector: FfiSelector, + pub data: FfiVecU8, +} + +impl From for FfiEventRecord { + fn from(value: EventRecord) -> Self { + let EventRecord { + block_id, + tx_index, + tx_hash, + program_id, + selector, + data, + } = value; + + Self { + block_id, + tx_index, + tx_hash: tx_hash.into(), + program_id: program_id.into(), + selector: selector.into(), + data: data.into(), + } + } +} + +/// Frees the resources associated with the given vector of ffi event records. +/// +/// Takes ownership of the whole allocation produced by `query_events`: the outer +/// `Box>` (the `PointerResult.value` pointer), the vector's +/// backing buffer, and every record's payload within it. +/// +/// # Arguments +/// +/// - `val`: The `*mut FfiVec` returned in `PointerResult.value`. +/// +/// # Returns +/// +/// void. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `val` is a pointer to an `FfiVec` produced by this library and not yet freed. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn free_ffi_event_record_vec(val: *mut FfiVec) { + if val.is_null() { + log::error!("Trying to free a null pointer. Exiting"); + return; + } + let ffi_vec = unsafe { Box::from_raw(val) }; + let records: Vec = (*ffi_vec).into(); + for record in records { + drop(Vec::from(record.data)); + } +} diff --git a/lez/indexer/ffi/src/api/types/mod.rs b/lez/indexer/ffi/src/api/types/mod.rs index 0b3574e69..82a6c2fe2 100644 --- a/lez/indexer/ffi/src/api/types/mod.rs +++ b/lez/indexer/ffi/src/api/types/mod.rs @@ -1,7 +1,8 @@ -use indexer_service_protocol::{AccountId, HashType, ProgramId, PublicKey, Signature}; +use indexer_service_protocol::{AccountId, HashType, ProgramId, PublicKey, Selector, Signature}; pub mod account; pub mod block; +pub mod event; pub mod transaction; pub mod vectors; @@ -12,6 +13,13 @@ pub struct FfiBytes32 { pub data: [u8; 32], } +/// 8-byte array type for event selectors. +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct FfiBytes8 { + pub data: [u8; 8], +} + /// 64-byte array type for signatures, etc. #[repr(C)] #[derive(Clone, Copy)] @@ -74,6 +82,7 @@ pub type FfiSignature = FfiBytes64; pub type FfiAccountId = FfiBytes32; pub type FfiNonce = FfiU128; pub type FfiPublicKey = FfiBytes32; +pub type FfiSelector = FfiBytes8; impl From for FfiHashType { fn from(value: HashType) -> Self { @@ -93,6 +102,12 @@ impl From for FfiAccountId { } } +impl From for FfiSelector { + fn from(value: Selector) -> Self { + Self { data: value.0 } + } +} + impl From for FfiPublicKey { fn from(value: PublicKey) -> Self { Self { data: value.0 } diff --git a/lez/indexer/ffi/src/errors.rs b/lez/indexer/ffi/src/errors.rs index 4572474ca..136fc9433 100644 --- a/lez/indexer/ffi/src/errors.rs +++ b/lez/indexer/ffi/src/errors.rs @@ -6,6 +6,7 @@ pub enum OperationStatus { NullPointer = 0x1, InitializationError = 0x2, ClientError = 0x3, + InvalidArgument = 0x4, } impl OperationStatus { diff --git a/lez/indexer/ffi/src/lib.rs b/lez/indexer/ffi/src/lib.rs index 0ca197c7b..bdd5709e7 100644 --- a/lez/indexer/ffi/src/lib.rs +++ b/lez/indexer/ffi/src/lib.rs @@ -8,3 +8,12 @@ pub mod api; mod errors; mod indexer; mod runtime; + +/// Largest block span a single `query_events` range request may cover. +// Spelled as a literal so cbindgen can emit it into the header for C callers; the +// assertion below makes any drift from the protocol crate's value a compile error. +pub const MAX_EVENT_QUERY_BLOCK_SPAN: u64 = 1000; +const _: () = assert!( + MAX_EVENT_QUERY_BLOCK_SPAN == indexer_service_protocol::MAX_EVENT_QUERY_BLOCK_SPAN, + "FFI event-query span cap must match the protocol crate's" +); diff --git a/lez/indexer/service/protocol/src/lib.rs b/lez/indexer/service/protocol/src/lib.rs index 7ba95b1fc..f2d7dc5a9 100644 --- a/lez/indexer/service/protocol/src/lib.rs +++ b/lez/indexer/service/protocol/src/lib.rs @@ -48,7 +48,8 @@ mod base64 { } } -// Bounds the server work one range query can request. +// Largest block span a single events range query may cover. Lives here so every surface +// that serves the query (RPC service, FFI) enforces the identical bound. pub const MAX_EVENT_QUERY_BLOCK_SPAN: u64 = 1000; pub type Nonce = u128;