feat(indexer): expose events over FFI

This commit is contained in:
Artem Gureev
2026-08-23 06:02:47 +00:00
parent 6638fcb5e3
commit b11254e027
7 changed files with 330 additions and 5 deletions
+102
View File
@@ -5,11 +5,17 @@
#include <stdint.h>
#include <stdlib.h>
/**
* 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<u64>` - 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<FfiEventRecord>` 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<FfiVec<FfiEventRecord>>` (the `PointerResult.value` pointer), the vector's
* backing buffer, and every record's payload within it.
*
* # Arguments
*
* - `val`: The `*mut FfiVec<FfiEventRecord>` returned in `PointerResult.value`.
*
* # Returns
*
* void.
*
* # Safety
*
* The caller must ensure that:
* - `val` is a pointer to an `FfiVec<FfiEventRecord>` 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.
*
+132 -3
View File
@@ -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<u64>` - 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<FfiEventRecord>` 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<u64>,
tx_hash: *const FfiHashType,
program_id: *const FfiProgramId,
selector: *const FfiSelector,
) -> PointerResult<FfiVec<FfiEventRecord>, 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::<Vec<_>>()
})
};
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::<Vec<FfiEventRecord>>()
.into(),
)
},
)
}
+68
View File
@@ -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<EventRecord> 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<FfiVec<FfiEventRecord>>` (the `PointerResult.value` pointer), the vector's
/// backing buffer, and every record's payload within it.
///
/// # Arguments
///
/// - `val`: The `*mut FfiVec<FfiEventRecord>` returned in `PointerResult.value`.
///
/// # Returns
///
/// void.
///
/// # Safety
///
/// The caller must ensure that:
/// - `val` is a pointer to an `FfiVec<FfiEventRecord>` produced by this library and not yet freed.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn free_ffi_event_record_vec(val: *mut FfiVec<FfiEventRecord>) {
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<FfiEventRecord> = (*ffi_vec).into();
for record in records {
drop(Vec::from(record.data));
}
}
+16 -1
View File
@@ -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<HashType> for FfiHashType {
fn from(value: HashType) -> Self {
@@ -93,6 +102,12 @@ impl From<AccountId> for FfiAccountId {
}
}
impl From<Selector> for FfiSelector {
fn from(value: Selector) -> Self {
Self { data: value.0 }
}
}
impl From<PublicKey> for FfiPublicKey {
fn from(value: PublicKey) -> Self {
Self { data: value.0 }
+1
View File
@@ -6,6 +6,7 @@ pub enum OperationStatus {
NullPointer = 0x1,
InitializationError = 0x2,
ClientError = 0x3,
InvalidArgument = 0x4,
}
impl OperationStatus {
+9
View File
@@ -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"
);
+2 -1
View File
@@ -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;