diff --git a/.deny.toml b/.deny.toml index e65cdd34..57b5f759 100644 --- a/.deny.toml +++ b/.deny.toml @@ -14,6 +14,8 @@ ignore = [ { id = "RUSTSEC-2025-0141", reason = "`bincode` is unmaintained but continuing to use it." }, { id = "RUSTSEC-2023-0089", reason = "atomic-polyfill is pulled transitively via risc0-zkvm; waiting on upstream fix (see https://github.com/risc0/risc0/issues/3453)" }, { id = "RUSTSEC-2026-0097", reason = "`rand` v0.8.5 is present transitively from logos crates, modification may break integration" }, + { id = "RUSTSEC-2026-0118", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration"}, + { id = "RUSTSEC-2026-0119", reason = "`hickory-proto` v0.25.0-alpha.5 is present transitively from logos crates, modification may break integration"}, ] yanked = "deny" unused-ignored-advisory = "deny" diff --git a/Cargo.lock b/Cargo.lock index 95e489fd..1c49d7e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2064,7 +2064,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 2.0.117", + "syn 1.0.109", ] [[package]] @@ -2558,7 +2558,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3797,10 +3797,16 @@ dependencies = [ name = "indexer_ffi" version = "0.1.0" dependencies = [ + "anyhow", "cbindgen", "indexer_service", + "indexer_service_protocol", + "indexer_service_rpc", + "jsonrpsee", "log", + "nssa", "tokio", + "url", ] [[package]] @@ -3916,6 +3922,7 @@ dependencies = [ "hex", "indexer_ffi", "indexer_service", + "indexer_service_protocol", "indexer_service_rpc", "jsonrpsee", "key_protocol", @@ -6335,7 +6342,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -8120,7 +8127,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -9089,7 +9096,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -10375,7 +10382,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 4fcaf9f6..1bce967f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ members = [ "examples/program_deployment/methods", "examples/program_deployment/methods/guest", "testnet_initial_state", - "indexer_ffi", + "indexer/ffi", ] [workspace.dependencies] @@ -57,7 +57,7 @@ indexer_service_protocol = { path = "indexer/service/protocol" } indexer_service_rpc = { path = "indexer/service/rpc" } wallet = { path = "wallet" } wallet-ffi = { path = "wallet-ffi", default-features = false } -indexer_ffi = { path = "indexer_ffi" } +indexer_ffi = { path = "indexer/ffi" } clock_core = { path = "programs/clock/core" } token_core = { path = "programs/token/core" } token_program = { path = "programs/token" } diff --git a/indexer_ffi/Cargo.toml b/indexer/ffi/Cargo.toml similarity index 71% rename from indexer_ffi/Cargo.toml rename to indexer/ffi/Cargo.toml index b55230c6..1e6b1468 100644 --- a/indexer_ffi/Cargo.toml +++ b/indexer/ffi/Cargo.toml @@ -5,9 +5,16 @@ name = "indexer_ffi" version = "0.1.0" [dependencies] +nssa.workspace = true indexer_service.workspace = true +indexer_service_rpc = { workspace = true, features = ["client"] } +indexer_service_protocol.workspace = true + +url.workspace = true log = { workspace = true } tokio = { features = ["rt-multi-thread"], workspace = true } +jsonrpsee.workspace = true +anyhow.workspace = true [build-dependencies] cbindgen = "0.29" diff --git a/indexer_ffi/build.rs b/indexer/ffi/build.rs similarity index 100% rename from indexer_ffi/build.rs rename to indexer/ffi/build.rs diff --git a/indexer_ffi/cbindgen.toml b/indexer/ffi/cbindgen.toml similarity index 100% rename from indexer_ffi/cbindgen.toml rename to indexer/ffi/cbindgen.toml diff --git a/indexer/ffi/indexer_ffi.h b/indexer/ffi/indexer_ffi.h new file mode 100644 index 00000000..7626b3b3 --- /dev/null +++ b/indexer/ffi/indexer_ffi.h @@ -0,0 +1,685 @@ +#include +#include +#include +#include + +typedef enum OperationStatus { + Ok = 0, + NullPointer = 1, + InitializationError = 2, + ClientError = 3, +} OperationStatus; + +typedef enum FfiTransactionKind { + Public = 0, + Private, + ProgramDeploy, +} FfiTransactionKind; + +typedef enum FfiBedrockStatus { + Pending = 0, + Safe, + Finalized, +} FfiBedrockStatus; + +typedef struct IndexerServiceFFI { + void *indexer_handle; + void *runtime; + void *indexer_client; +} IndexerServiceFFI; + +/** + * 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_IndexerServiceFFI__OperationStatus { + struct IndexerServiceFFI *value; + enum OperationStatus error; +} PointerResult_IndexerServiceFFI__OperationStatus; + +typedef struct PointerResult_IndexerServiceFFI__OperationStatus InitializedIndexerServiceFFIResult; + +/** + * 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_u64__OperationStatus { + uint64_t *value; + enum OperationStatus error; +} PointerResult_u64__OperationStatus; + +typedef uint64_t FfiBlockId; + +/** + * 32-byte array type for `AccountId`, keys, hashes, etc. + */ +typedef struct FfiBytes32 { + uint8_t data[32]; +} FfiBytes32; + +typedef struct FfiBytes32 FfiHashType; + +typedef uint64_t FfiTimestamp; + +/** + * 64-byte array type for signatures, etc. + */ +typedef struct FfiBytes64 { + uint8_t data[64]; +} FfiBytes64; + +typedef struct FfiBytes64 FfiSignature; + +typedef struct FfiBlockHeader { + FfiBlockId block_id; + FfiHashType prev_block_hash; + FfiHashType hash; + FfiTimestamp timestamp; + FfiSignature signature; +} FfiBlockHeader; + +/** + * Program ID - 8 u32 values (32 bytes total). + */ +typedef struct FfiProgramId { + uint32_t data[8]; +} FfiProgramId; + +typedef struct FfiBytes32 FfiAccountId; + +typedef struct FfiVec_FfiAccountId { + FfiAccountId *entries; + uintptr_t len; + uintptr_t capacity; +} FfiVec_FfiAccountId; + +typedef struct FfiVec_FfiAccountId FfiAccountIdList; + +/** + * U128 - 16 bytes little endian. + */ +typedef struct FfiU128 { + uint8_t data[16]; +} FfiU128; + +typedef struct FfiU128 FfiNonce; + +typedef struct FfiVec_FfiNonce { + FfiNonce *entries; + uintptr_t len; + uintptr_t capacity; +} FfiVec_FfiNonce; + +typedef struct FfiVec_FfiNonce FfiNonceList; + +typedef struct FfiVec_u32 { + uint32_t *entries; + uintptr_t len; + uintptr_t capacity; +} FfiVec_u32; + +typedef struct FfiVec_u32 FfiInstructionDataList; + +typedef struct FfiPublicMessage { + struct FfiProgramId program_id; + FfiAccountIdList account_ids; + FfiNonceList nonces; + FfiInstructionDataList instruction_data; +} FfiPublicMessage; + +typedef struct FfiBytes32 FfiPublicKey; + +typedef struct FfiSignaturePubKeyEntry { + FfiSignature signature; + FfiPublicKey public_key; +} FfiSignaturePubKeyEntry; + +typedef struct FfiVec_FfiSignaturePubKeyEntry { + struct FfiSignaturePubKeyEntry *entries; + uintptr_t len; + uintptr_t capacity; +} FfiVec_FfiSignaturePubKeyEntry; + +typedef struct FfiVec_FfiSignaturePubKeyEntry FfiSignaturePubKeyList; + +typedef struct FfiPublicTransactionBody { + FfiHashType hash; + struct FfiPublicMessage message; + FfiSignaturePubKeyList witness_set; +} FfiPublicTransactionBody; + +/** + * Account data structure - C-compatible version of nssa Account. + * + * Note: `balance` and `nonce` are u128 values represented as little-endian + * byte arrays since C doesn't have native u128 support. + */ +typedef struct FfiAccount { + struct FfiProgramId program_owner; + /** + * Balance as little-endian [u8; 16]. + */ + struct FfiU128 balance; + /** + * Pointer to account data bytes. + */ + uint8_t *data; + /** + * Length of account data. + */ + uintptr_t data_len; + /** + * Capacity of account data. + */ + uintptr_t data_cap; + /** + * Nonce as little-endian [u8; 16]. + */ + struct FfiU128 nonce; +} FfiAccount; + +typedef struct FfiVec_FfiAccount { + struct FfiAccount *entries; + uintptr_t len; + uintptr_t capacity; +} FfiVec_FfiAccount; + +typedef struct FfiVec_FfiAccount FfiAccountList; + +typedef struct FfiVec_u8 { + uint8_t *entries; + uintptr_t len; + uintptr_t capacity; +} FfiVec_u8; + +typedef struct FfiVec_u8 FfiVecU8; + +typedef struct FfiEncryptedAccountData { + FfiVecU8 ciphertext; + FfiVecU8 epk; + uint8_t view_tag; +} FfiEncryptedAccountData; + +typedef struct FfiVec_FfiEncryptedAccountData { + struct FfiEncryptedAccountData *entries; + uintptr_t len; + uintptr_t capacity; +} FfiVec_FfiEncryptedAccountData; + +typedef struct FfiVec_FfiEncryptedAccountData FfiEncryptedAccountDataList; + +typedef struct FfiVec_FfiBytes32 { + struct FfiBytes32 *entries; + uintptr_t len; + uintptr_t capacity; +} FfiVec_FfiBytes32; + +typedef struct FfiVec_FfiBytes32 FfiVecBytes32; + +typedef struct FfiNullifierCommitmentSet { + struct FfiBytes32 nullifier; + struct FfiBytes32 commitment_set_digest; +} FfiNullifierCommitmentSet; + +typedef struct FfiVec_FfiNullifierCommitmentSet { + struct FfiNullifierCommitmentSet *entries; + uintptr_t len; + uintptr_t capacity; +} FfiVec_FfiNullifierCommitmentSet; + +typedef struct FfiVec_FfiNullifierCommitmentSet FfiNullifierCommitmentSetList; + +typedef struct FfiPrivacyPreservingMessage { + FfiAccountIdList public_account_ids; + FfiNonceList nonces; + FfiAccountList public_post_states; + FfiEncryptedAccountDataList encrypted_private_post_states; + FfiVecBytes32 new_commitments; + FfiNullifierCommitmentSetList new_nullifiers; + uint64_t block_validity_window[2]; + uint64_t timestamp_validity_window[2]; +} FfiPrivacyPreservingMessage; + +typedef FfiVecU8 FfiProof; + +typedef struct FfiPrivateTransactionBody { + FfiHashType hash; + struct FfiPrivacyPreservingMessage message; + FfiSignaturePubKeyList witness_set; + FfiProof proof; +} FfiPrivateTransactionBody; + +typedef FfiVecU8 FfiProgramDeploymentMessage; + +typedef struct FfiProgramDeploymentTransactionBody { + FfiHashType hash; + FfiProgramDeploymentMessage message; +} FfiProgramDeploymentTransactionBody; + +typedef struct FfiTransactionBody { + struct FfiPublicTransactionBody *public_body; + struct FfiPrivateTransactionBody *private_body; + struct FfiProgramDeploymentTransactionBody *program_deployment_body; +} FfiTransactionBody; + +typedef struct FfiTransaction { + struct FfiTransactionBody body; + enum FfiTransactionKind kind; +} FfiTransaction; + +typedef struct FfiVec_FfiTransaction { + struct FfiTransaction *entries; + uintptr_t len; + uintptr_t capacity; +} FfiVec_FfiTransaction; + +typedef struct FfiVec_FfiTransaction FfiBlockBody; + +typedef struct FfiBytes32 FfiMsgId; + +typedef struct FfiBlock { + struct FfiBlockHeader header; + FfiBlockBody body; + enum FfiBedrockStatus bedrock_status; + FfiMsgId bedrock_parent_id; +} FfiBlock; + +typedef struct FfiOption_FfiBlock { + struct FfiBlock *value; + bool is_some; +} FfiOption_FfiBlock; + +typedef struct FfiOption_FfiBlock FfiBlockOpt; + +/** + * 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_FfiBlockOpt__OperationStatus { + FfiBlockOpt *value; + enum OperationStatus error; +} PointerResult_FfiBlockOpt__OperationStatus; + +/** + * 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_FfiAccount__OperationStatus { + struct FfiAccount *value; + enum OperationStatus error; +} PointerResult_FfiAccount__OperationStatus; + +typedef struct FfiOption_FfiTransaction { + struct FfiTransaction *value; + bool is_some; +} FfiOption_FfiTransaction; + +/** + * 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_FfiOption_FfiTransaction_____OperationStatus { + struct FfiOption_FfiTransaction *value; + enum OperationStatus error; +} PointerResult_FfiOption_FfiTransaction_____OperationStatus; + +typedef struct FfiVec_FfiBlock { + struct FfiBlock *entries; + uintptr_t len; + uintptr_t capacity; +} FfiVec_FfiBlock; + +/** + * 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_FfiBlock_____OperationStatus { + struct FfiVec_FfiBlock *value; + enum OperationStatus error; +} PointerResult_FfiVec_FfiBlock_____OperationStatus; + +typedef struct FfiOption_u64 { + uint64_t *value; + bool is_some; +} FfiOption_u64; + +/** + * 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_FfiTransaction_____OperationStatus { + struct FfiVec_FfiTransaction *value; + enum OperationStatus error; +} PointerResult_FfiVec_FfiTransaction_____OperationStatus; + +/** + * Creates and starts an indexer based on the provided + * configuration file path. + * + * # Arguments + * + * - `config_path`: A pointer to a string representing the path to the configuration file. + * - `port`: Number representing a port, on which indexers RPC will start. + * + * # Returns + * + * An `InitializedIndexerServiceFFIResult` containing either a pointer to the + * initialized `IndexerServiceFFI` or an error code. + */ +InitializedIndexerServiceFFIResult start_indexer(const char *config_path, uint16_t port); + +/** + * Stops and frees the resources associated with the given indexer service. + * + * # Arguments + * + * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be stopped. + * + * # Returns + * + * An `OperationStatus` indicating success or failure. + * + * # Safety + * + * The caller must ensure that: + * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance + * - The `IndexerServiceFFI` instance was created by this library + * - The pointer will not be used after this function returns + */ +enum OperationStatus stop_indexer(struct IndexerServiceFFI *indexer); + +/** + * # Safety + * It's up to the caller to pass a proper pointer, if somehow from c/c++ side + * this is called with a type which doesn't come from a returned `CString` it + * will cause a segfault. + */ +void free_cstring(char *block); + +/** + * Query the last block id from indexer. + * + * # Arguments + * + * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. + * + * # Returns + * + * A `PointerResult` indicating success or failure. + * + * # Safety + * + * The caller must ensure that: + * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance + */ +struct PointerResult_u64__OperationStatus query_last_block(const struct IndexerServiceFFI *indexer); + +/** + * Query the block by id from indexer. + * + * # Arguments + * + * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. + * - `block_id`: `u64` number of block id + * + * # Returns + * + * A `PointerResult` indicating success or failure. + * + * # Safety + * + * The caller must ensure that: + * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance + */ +struct PointerResult_FfiBlockOpt__OperationStatus query_block(const struct IndexerServiceFFI *indexer, + FfiBlockId block_id); + +/** + * Query the block by id from indexer. + * + * # Arguments + * + * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. + * - `hash`: `FfiHashType` - hash of block + * + * # Returns + * + * A `PointerResult` indicating success or failure. + * + * # Safety + * + * The caller must ensure that: + * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance + */ +struct PointerResult_FfiBlockOpt__OperationStatus query_block_by_hash(const struct IndexerServiceFFI *indexer, + FfiHashType hash); + +/** + * Query the account by id from indexer. + * + * # Arguments + * + * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. + * - `account_id`: `FfiAccountId` - id of queried account + * + * # Returns + * + * A `PointerResult` indicating success or failure. + * + * # Safety + * + * The caller must ensure that: + * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance + */ +struct PointerResult_FfiAccount__OperationStatus query_account(const struct IndexerServiceFFI *indexer, + FfiAccountId account_id); + +/** + * Query the trasnaction by hash from indexer. + * + * # Arguments + * + * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. + * - `hash`: `FfiHashType` - hash of transaction + * + * # Returns + * + * A `PointerResult, OperationStatus>` indicating success or failure. + * + * # Safety + * + * The caller must ensure that: + * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance + */ +struct PointerResult_FfiOption_FfiTransaction_____OperationStatus query_transaction(const struct IndexerServiceFFI *indexer, + FfiHashType hash); + +/** + * Query the blocks by block range from indexer. + * + * # Arguments + * + * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. + * - `before`: `FfiOption` - end block of query + * - `limit`: `u64` - number of blocks to query before `before` + * + * # Returns + * + * A `PointerResult, OperationStatus>` indicating success or failure. + * + * # Safety + * + * The caller must ensure that: + * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance + */ +struct PointerResult_FfiVec_FfiBlock_____OperationStatus query_block_vec(const struct IndexerServiceFFI *indexer, + struct FfiOption_u64 before, + uint64_t limit); + +/** + * Query the transactions range by account id from indexer. + * + * # Arguments + * + * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. + * - `account_id`: `FfiAccountId` - id of queried account + * - `offset`: `u64` - first tx id of query + * - `limit`: `u64` - number of tx ids to query after `offset` + * + * # Returns + * + * A `PointerResult, OperationStatus>` indicating success or failure. + * + * # Safety + * + * The caller must ensure that: + * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance + */ +struct PointerResult_FfiVec_FfiTransaction_____OperationStatus query_transactions_by_account(const struct IndexerServiceFFI *indexer, + FfiAccountId account_id, + uint64_t offset, + uint64_t limit); + +/** + * Frees the resources associated with the given ffi account. + * + * # Arguments + * + * - `val`: An instance of `FfiAccount`. + * + * # Returns + * + * void. + * + * # Safety + * + * The caller must ensure that: + * - `val` is a valid instance of `FfiAccount`. + */ +void free_ffi_account(struct FfiAccount val); + +/** + * Frees the resources associated with the given ffi block. + * + * # Arguments + * + * - `val`: An instance of `FfiBlock`. + * + * # Returns + * + * void. + * + * # Safety + * + * The caller must ensure that: + * - `val` is a valid instance of `FfiBlock`. + */ +void free_ffi_block(struct FfiBlock val); + +/** + * Frees the resources associated with the given ffi block option. + * + * # Arguments + * + * - `val`: An instance of `FfiBlockOpt`. + * + * # Returns + * + * void. + * + * # Safety + * + * The caller must ensure that: + * - `val` is a valid instance of `FfiBlockOpt`. + */ +void free_ffi_block_opt(FfiBlockOpt val); + +/** + * Frees the resources associated with the given ffi block vector. + * + * # Arguments + * + * - `val`: An instance of `FfiVec`. + * + * # Returns + * + * void. + * + * # Safety + * + * The caller must ensure that: + * - `val` is a valid instance of `FfiVec`. + */ +void free_ffi_block_vec(struct FfiVec_FfiBlock val); + +/** + * Frees the resources associated with the given ffi transaction. + * + * # Arguments + * + * - `val`: An instance of `FfiTransaction`. + * + * # Returns + * + * void. + * + * # Safety + * + * The caller must ensure that: + * - `val` is a valid instance of `FfiTransaction`. + */ +void free_ffi_transaction(struct FfiTransaction val); + +/** + * Frees the resources associated with the given ffi transaction option. + * + * # Arguments + * + * - `val`: An instance of `FfiOption`. + * + * # Returns + * + * void. + * + * # Safety + * + * The caller must ensure that: + * - `val` is a valid instance of `FfiOption`. + */ +void free_ffi_transaction_opt(struct FfiOption_FfiTransaction val); + +/** + * Frees the resources associated with the given vector of ffi transactions. + * + * # Arguments + * + * - `val`: An instance of `FfiVec`. + * + * # Returns + * + * void. + * + * # Safety + * + * The caller must ensure that: + * - `val` is a valid instance of `FfiVec`. + */ +void free_ffi_transaction_vec(struct FfiVec_FfiTransaction val); + +bool is_ok(const enum OperationStatus *self); + +bool is_error(const enum OperationStatus *self); diff --git a/indexer/ffi/src/api/client.rs b/indexer/ffi/src/api/client.rs new file mode 100644 index 00000000..825a57de --- /dev/null +++ b/indexer/ffi/src/api/client.rs @@ -0,0 +1,36 @@ +use std::net::SocketAddr; + +use url::Url; + +use crate::OperationStatus; + +#[derive(Debug, Clone, Copy)] +pub enum UrlProtocol { + Http, + Ws, +} + +impl std::fmt::Display for UrlProtocol { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Http => write!(f, "http"), + Self::Ws => write!(f, "ws"), + } + } +} + +pub(crate) fn addr_to_url(protocol: UrlProtocol, addr: SocketAddr) -> Result { + // Convert 0.0.0.0 to 127.0.0.1 for client connections + // When binding to port 0, the server binds to 0.0.0.0: + // but clients need to connect to 127.0.0.1: to work reliably + let url_string = if addr.ip().is_unspecified() { + format!("{protocol}://127.0.0.1:{}", addr.port()) + } else { + format!("{protocol}://{addr}") + }; + + url_string.parse().map_err(|e| { + log::error!("Could not parse indexer url: {e}"); + OperationStatus::InitializationError + }) +} diff --git a/indexer_ffi/src/api/lifecycle.rs b/indexer/ffi/src/api/lifecycle.rs similarity index 84% rename from indexer_ffi/src/api/lifecycle.rs rename to indexer/ffi/src/api/lifecycle.rs index 735efd4d..c9cd859d 100644 --- a/indexer_ffi/src/api/lifecycle.rs +++ b/indexer/ffi/src/api/lifecycle.rs @@ -2,7 +2,15 @@ use std::{ffi::c_char, path::PathBuf}; use tokio::runtime::Runtime; -use crate::{IndexerServiceFFI, api::PointerResult, errors::OperationStatus}; +use crate::{ + IndexerServiceFFI, + api::{ + PointerResult, + client::{UrlProtocol, addr_to_url}, + }, + client::{IndexerClient, IndexerClientTrait as _}, + errors::OperationStatus, +}; pub type InitializedIndexerServiceFFIResult = PointerResult; @@ -67,7 +75,13 @@ fn setup_indexer( OperationStatus::InitializationError })?; - Ok(IndexerServiceFFI::new(indexer_handle, rt)) + let indexer_url = addr_to_url(UrlProtocol::Ws, indexer_handle.addr())?; + let indexer_client = rt.block_on(IndexerClient::new(&indexer_url)).map_err(|e| { + log::error!("Could not start indexer client: {e}"); + OperationStatus::InitializationError + })?; + + Ok(IndexerServiceFFI::new(indexer_handle, rt, indexer_client)) } /// Stops and frees the resources associated with the given indexer service. diff --git a/indexer_ffi/src/api/memory.rs b/indexer/ffi/src/api/memory.rs similarity index 100% rename from indexer_ffi/src/api/memory.rs rename to indexer/ffi/src/api/memory.rs diff --git a/indexer_ffi/src/api/mod.rs b/indexer/ffi/src/api/mod.rs similarity index 64% rename from indexer_ffi/src/api/mod.rs rename to indexer/ffi/src/api/mod.rs index e84a3913..ea2b91d7 100644 --- a/indexer_ffi/src/api/mod.rs +++ b/indexer/ffi/src/api/mod.rs @@ -1,5 +1,8 @@ pub use result::PointerResult; +pub mod client; pub mod lifecycle; pub mod memory; +pub mod query; pub mod result; +pub mod types; diff --git a/indexer/ffi/src/api/query.rs b/indexer/ffi/src/api/query.rs new file mode 100644 index 00000000..1e39d961 --- /dev/null +++ b/indexer/ffi/src/api/query.rs @@ -0,0 +1,334 @@ +use indexer_service_protocol::{AccountId, HashType}; +use indexer_service_rpc::RpcClient as _; + +use crate::{ + IndexerServiceFFI, + api::{ + PointerResult, + types::{ + FfiAccountId, FfiBlockId, FfiHashType, FfiOption, FfiVec, + account::FfiAccount, + block::{FfiBlock, FfiBlockOpt}, + transaction::FfiTransaction, + }, + }, + errors::OperationStatus, +}; + +/// Query the last block id from indexer. +/// +/// # Arguments +/// +/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. +/// +/// # Returns +/// +/// A `PointerResult` indicating success or failure. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance +#[unsafe(no_mangle)] +pub unsafe extern "C" fn query_last_block( + indexer: *const IndexerServiceFFI, +) -> PointerResult { + 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 client = unsafe { indexer.client() }; + let runtime = unsafe { indexer.runtime() }; + + runtime + .block_on(client.get_last_finalized_block_id()) + .map_or_else( + |_| PointerResult::from_error(OperationStatus::ClientError), + PointerResult::from_value, + ) +} + +/// Query the block by id from indexer. +/// +/// # Arguments +/// +/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. +/// - `block_id`: `u64` number of block id +/// +/// # Returns +/// +/// A `PointerResult` indicating success or failure. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance +#[unsafe(no_mangle)] +pub unsafe extern "C" fn query_block( + indexer: *const IndexerServiceFFI, + block_id: FfiBlockId, +) -> PointerResult { + 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 client = unsafe { indexer.client() }; + let runtime = unsafe { indexer.runtime() }; + + runtime + .block_on(client.get_block_by_id(block_id)) + .map_or_else( + |_| PointerResult::from_error(OperationStatus::ClientError), + |block_opt| { + let block_ffi = block_opt.map_or_else(FfiBlockOpt::from_none, |block| { + FfiBlockOpt::from_value(block.into()) + }); + + PointerResult::from_value(block_ffi) + }, + ) +} + +/// Query the block by id from indexer. +/// +/// # Arguments +/// +/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. +/// - `hash`: `FfiHashType` - hash of block +/// +/// # Returns +/// +/// A `PointerResult` indicating success or failure. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance +#[unsafe(no_mangle)] +pub unsafe extern "C" fn query_block_by_hash( + indexer: *const IndexerServiceFFI, + hash: FfiHashType, +) -> PointerResult { + 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 client = unsafe { indexer.client() }; + let runtime = unsafe { indexer.runtime() }; + + runtime + .block_on(client.get_block_by_hash(HashType(hash.data))) + .map_or_else( + |_| PointerResult::from_error(OperationStatus::ClientError), + |block_opt| { + let block_ffi = block_opt.map_or_else(FfiBlockOpt::from_none, |block| { + FfiBlockOpt::from_value(block.into()) + }); + + PointerResult::from_value(block_ffi) + }, + ) +} + +/// Query the account by id from indexer. +/// +/// # Arguments +/// +/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. +/// - `account_id`: `FfiAccountId` - id of queried account +/// +/// # Returns +/// +/// A `PointerResult` indicating success or failure. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance +#[unsafe(no_mangle)] +pub unsafe extern "C" fn query_account( + indexer: *const IndexerServiceFFI, + account_id: FfiAccountId, +) -> PointerResult { + 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 client = unsafe { indexer.client() }; + let runtime = unsafe { indexer.runtime() }; + + runtime + .block_on(client.get_account(AccountId { + value: account_id.data, + })) + .map_or_else( + |_| PointerResult::from_error(OperationStatus::ClientError), + |acc| { + let acc_nssa: nssa::Account = + acc.try_into().expect("Source is in blocks, must fit"); + PointerResult::from_value(acc_nssa.into()) + }, + ) +} + +/// Query the trasnaction by hash from indexer. +/// +/// # Arguments +/// +/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. +/// - `hash`: `FfiHashType` - hash of transaction +/// +/// # Returns +/// +/// A `PointerResult, OperationStatus>` indicating success or failure. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance +#[unsafe(no_mangle)] +pub unsafe extern "C" fn query_transaction( + indexer: *const IndexerServiceFFI, + hash: FfiHashType, +) -> 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 client = unsafe { indexer.client() }; + let runtime = unsafe { indexer.runtime() }; + + runtime + .block_on(client.get_transaction(HashType(hash.data))) + .map_or_else( + |_| PointerResult::from_error(OperationStatus::ClientError), + |tx_opt| { + let tx_ffi = tx_opt.map_or_else(FfiOption::::from_none, |tx| { + FfiOption::::from_value(tx.into()) + }); + + PointerResult::from_value(tx_ffi) + }, + ) +} + +/// Query the blocks by block range from indexer. +/// +/// # Arguments +/// +/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. +/// - `before`: `FfiOption` - end block of query +/// - `limit`: `u64` - number of blocks to query before `before` +/// +/// # Returns +/// +/// A `PointerResult, OperationStatus>` indicating success or failure. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance +#[unsafe(no_mangle)] +pub unsafe extern "C" fn query_block_vec( + indexer: *const IndexerServiceFFI, + before: FfiOption, + limit: u64, +) -> 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 client = unsafe { indexer.client() }; + let runtime = unsafe { indexer.runtime() }; + + let before_std = before.is_some.then(|| unsafe { *before.value }); + + runtime + .block_on(client.get_blocks(before_std, limit)) + .map_or_else( + |_| PointerResult::from_error(OperationStatus::ClientError), + |block_vec| { + PointerResult::from_value( + block_vec + .into_iter() + .map(Into::into) + .collect::>() + .into(), + ) + }, + ) +} + +/// Query the transactions range by account id from indexer. +/// +/// # Arguments +/// +/// - `indexer`: A pointer to the `IndexerServiceFFI` instance to be queried. +/// - `account_id`: `FfiAccountId` - id of queried account +/// - `offset`: `u64` - first tx id of query +/// - `limit`: `u64` - number of tx ids to query after `offset` +/// +/// # Returns +/// +/// A `PointerResult, OperationStatus>` indicating success or failure. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `indexer` is a valid pointer to a `IndexerServiceFFI` instance +#[unsafe(no_mangle)] +pub unsafe extern "C" fn query_transactions_by_account( + indexer: *const IndexerServiceFFI, + account_id: FfiAccountId, + offset: u64, + limit: u64, +) -> 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 client = unsafe { indexer.client() }; + let runtime = unsafe { indexer.runtime() }; + + runtime + .block_on(client.get_transactions_by_account( + AccountId { + value: account_id.data, + }, + offset, + limit, + )) + .map_or_else( + |_| PointerResult::from_error(OperationStatus::ClientError), + |tx_vec| { + PointerResult::from_value( + tx_vec + .into_iter() + .map(Into::into) + .collect::>() + .into(), + ) + }, + ) +} diff --git a/indexer_ffi/src/api/result.rs b/indexer/ffi/src/api/result.rs similarity index 100% rename from indexer_ffi/src/api/result.rs rename to indexer/ffi/src/api/result.rs diff --git a/indexer/ffi/src/api/types/account.rs b/indexer/ffi/src/api/types/account.rs new file mode 100644 index 00000000..6c35347f --- /dev/null +++ b/indexer/ffi/src/api/types/account.rs @@ -0,0 +1,119 @@ +use indexer_service_protocol::ProgramId; + +use crate::api::types::{FfiBytes32, FfiProgramId, FfiU128}; + +/// Account data structure - C-compatible version of nssa Account. +/// +/// Note: `balance` and `nonce` are u128 values represented as little-endian +/// byte arrays since C doesn't have native u128 support. +#[repr(C)] +pub struct FfiAccount { + pub program_owner: FfiProgramId, + /// Balance as little-endian [u8; 16]. + pub balance: FfiU128, + /// Pointer to account data bytes. + pub data: *mut u8, + /// Length of account data. + pub data_len: usize, + /// Capacity of account data. + pub data_cap: usize, + /// Nonce as little-endian [u8; 16]. + pub nonce: FfiU128, +} + +// Helper functions to convert between Rust and FFI types + +impl From<&nssa::AccountId> for FfiBytes32 { + fn from(id: &nssa::AccountId) -> Self { + Self::from_account_id(id) + } +} + +impl From for FfiAccount { + fn from(value: nssa::Account) -> Self { + let nssa::Account { + program_owner, + balance, + data, + nonce, + } = value; + + let (data, data_len, data_cap) = data.into_inner().into_raw_parts(); + + let program_owner = FfiProgramId { + data: program_owner, + }; + Self { + program_owner, + balance: balance.into(), + data, + data_len, + data_cap, + nonce: nonce.0.into(), + } + } +} + +impl From for indexer_service_protocol::Account { + fn from(value: FfiAccount) -> Self { + let FfiAccount { + program_owner, + balance, + data, + data_cap, + data_len, + nonce, + } = value; + + Self { + program_owner: ProgramId(program_owner.data), + balance: balance.into(), + data: indexer_service_protocol::Data(unsafe { + Vec::from_raw_parts(data, data_len, data_cap) + }), + nonce: nonce.into(), + } + } +} + +impl From<&FfiAccount> for indexer_service_protocol::Account { + fn from(value: &FfiAccount) -> Self { + let &FfiAccount { + program_owner, + balance, + data, + data_cap, + data_len, + nonce, + } = value; + + Self { + program_owner: ProgramId(program_owner.data), + balance: balance.into(), + data: indexer_service_protocol::Data(unsafe { + Vec::from_raw_parts(data, data_len, data_cap) + }), + nonce: nonce.into(), + } + } +} + +/// Frees the resources associated with the given ffi account. +/// +/// # Arguments +/// +/// - `val`: An instance of `FfiAccount`. +/// +/// # Returns +/// +/// void. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `val` is a valid instance of `FfiAccount`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn free_ffi_account(val: FfiAccount) { + let orig_val: indexer_service_protocol::Account = val.into(); + drop(orig_val); +} diff --git a/indexer/ffi/src/api/types/block.rs b/indexer/ffi/src/api/types/block.rs new file mode 100644 index 00000000..bca2fdb5 --- /dev/null +++ b/indexer/ffi/src/api/types/block.rs @@ -0,0 +1,199 @@ +use indexer_service_protocol::{ + BedrockStatus, Block, BlockHeader, HashType, MantleMsgId, Signature, +}; + +use crate::api::types::{ + FfiBlockId, FfiHashType, FfiMsgId, FfiOption, FfiSignature, FfiTimestamp, FfiVec, + transaction::free_ffi_transaction_vec, vectors::FfiBlockBody, +}; + +#[repr(C)] +pub struct FfiBlock { + pub header: FfiBlockHeader, + pub body: FfiBlockBody, + pub bedrock_status: FfiBedrockStatus, + pub bedrock_parent_id: FfiMsgId, +} + +impl From for FfiBlock { + fn from(value: Block) -> Self { + let Block { + header, + body, + bedrock_status, + bedrock_parent_id, + } = value; + + Self { + header: header.into(), + body: body + .transactions + .into_iter() + .map(Into::into) + .collect::>() + .into(), + bedrock_status: bedrock_status.into(), + bedrock_parent_id: bedrock_parent_id.into(), + } + } +} + +pub type FfiBlockOpt = FfiOption; + +#[repr(C)] +pub struct FfiBlockHeader { + pub block_id: FfiBlockId, + pub prev_block_hash: FfiHashType, + pub hash: FfiHashType, + pub timestamp: FfiTimestamp, + pub signature: FfiSignature, +} + +impl From for FfiBlockHeader { + fn from(value: BlockHeader) -> Self { + let BlockHeader { + block_id, + prev_block_hash, + hash, + timestamp, + signature, + } = value; + + Self { + block_id, + prev_block_hash: prev_block_hash.into(), + hash: hash.into(), + timestamp, + signature: signature.into(), + } + } +} + +#[repr(C)] +pub enum FfiBedrockStatus { + Pending = 0x0, + Safe, + Finalized, +} + +impl From for FfiBedrockStatus { + fn from(value: BedrockStatus) -> Self { + match value { + BedrockStatus::Finalized => Self::Finalized, + BedrockStatus::Pending => Self::Pending, + BedrockStatus::Safe => Self::Safe, + } + } +} + +impl From for BedrockStatus { + fn from(value: FfiBedrockStatus) -> Self { + match value { + FfiBedrockStatus::Finalized => Self::Finalized, + FfiBedrockStatus::Pending => Self::Pending, + FfiBedrockStatus::Safe => Self::Safe, + } + } +} + +/// Frees the resources associated with the given ffi block. +/// +/// # Arguments +/// +/// - `val`: An instance of `FfiBlock`. +/// +/// # Returns +/// +/// void. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `val` is a valid instance of `FfiBlock`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn free_ffi_block(val: FfiBlock) { + // We don't really need all the casts, but just in case + // All except `ffi_tx_ffi_vec` is Copy types, so no need for Drop + let _ = BlockHeader { + block_id: val.header.block_id, + prev_block_hash: HashType(val.header.prev_block_hash.data), + hash: HashType(val.header.hash.data), + timestamp: val.header.timestamp, + signature: Signature(val.header.signature.data), + }; + let ffi_tx_ffi_vec = val.body; + + #[expect(clippy::let_underscore_must_use, reason = "No use for this Copy type")] + let _: BedrockStatus = val.bedrock_status.into(); + + let _ = MantleMsgId(val.bedrock_parent_id.data); + + unsafe { + free_ffi_transaction_vec(ffi_tx_ffi_vec); + }; +} + +/// Frees the resources associated with the given ffi block option. +/// +/// # Arguments +/// +/// - `val`: An instance of `FfiBlockOpt`. +/// +/// # Returns +/// +/// void. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `val` is a valid instance of `FfiBlockOpt`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn free_ffi_block_opt(val: FfiBlockOpt) { + if val.is_some { + let value = unsafe { Box::from_raw(val.value) }; + + // We don't really need all the casts, but just in case + // All except `ffi_tx_ffi_vec` is Copy types, so no need for Drop + let _ = BlockHeader { + block_id: value.header.block_id, + prev_block_hash: HashType(value.header.prev_block_hash.data), + hash: HashType(value.header.hash.data), + timestamp: value.header.timestamp, + signature: Signature(value.header.signature.data), + }; + let ffi_tx_ffi_vec = value.body; + + #[expect(clippy::let_underscore_must_use, reason = "No use for this Copy type")] + let _: BedrockStatus = value.bedrock_status.into(); + + let _ = MantleMsgId(value.bedrock_parent_id.data); + + unsafe { + free_ffi_transaction_vec(ffi_tx_ffi_vec); + }; + } +} + +/// Frees the resources associated with the given ffi block vector. +/// +/// # Arguments +/// +/// - `val`: An instance of `FfiVec`. +/// +/// # Returns +/// +/// void. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `val` is a valid instance of `FfiVec`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn free_ffi_block_vec(val: FfiVec) { + let ffi_block_std_vec: Vec<_> = val.into(); + for block in ffi_block_std_vec { + unsafe { + free_ffi_block(block); + } + } +} diff --git a/indexer/ffi/src/api/types/mod.rs b/indexer/ffi/src/api/types/mod.rs new file mode 100644 index 00000000..2e7a77ad --- /dev/null +++ b/indexer/ffi/src/api/types/mod.rs @@ -0,0 +1,165 @@ +use indexer_service_protocol::{AccountId, HashType, MantleMsgId, ProgramId, PublicKey, Signature}; + +pub mod account; +pub mod block; +pub mod transaction; +pub mod vectors; + +/// 32-byte array type for `AccountId`, keys, hashes, etc. +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct FfiBytes32 { + pub data: [u8; 32], +} + +/// 64-byte array type for signatures, etc. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FfiBytes64 { + pub data: [u8; 64], +} + +/// Program ID - 8 u32 values (32 bytes total). +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct FfiProgramId { + pub data: [u32; 8], +} + +impl From for FfiProgramId { + fn from(value: ProgramId) -> Self { + Self { data: value.0 } + } +} + +/// U128 - 16 bytes little endian. +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct FfiU128 { + pub data: [u8; 16], +} + +impl FfiBytes32 { + /// Create from a 32-byte array. + #[must_use] + pub const fn from_bytes(bytes: [u8; 32]) -> Self { + Self { data: bytes } + } + + /// Create from an `AccountId`. + #[must_use] + pub const fn from_account_id(id: &nssa::AccountId) -> Self { + Self { data: *id.value() } + } +} + +impl From for FfiU128 { + fn from(value: u128) -> Self { + Self { + data: value.to_le_bytes(), + } + } +} + +impl From for u128 { + fn from(value: FfiU128) -> Self { + Self::from_le_bytes(value.data) + } +} + +pub type FfiHashType = FfiBytes32; +pub type FfiMsgId = FfiBytes32; +pub type FfiBlockId = u64; +pub type FfiTimestamp = u64; +pub type FfiSignature = FfiBytes64; +pub type FfiAccountId = FfiBytes32; +pub type FfiNonce = FfiU128; +pub type FfiPublicKey = FfiBytes32; + +impl From for FfiHashType { + fn from(value: HashType) -> Self { + Self { data: value.0 } + } +} + +impl From for FfiMsgId { + fn from(value: MantleMsgId) -> Self { + Self { data: value.0 } + } +} + +impl From for FfiSignature { + fn from(value: Signature) -> Self { + Self { data: value.0 } + } +} + +impl From for FfiAccountId { + fn from(value: AccountId) -> Self { + Self { data: value.value } + } +} + +impl From for FfiPublicKey { + fn from(value: PublicKey) -> Self { + Self { data: value.0 } + } +} + +#[repr(C)] +pub struct FfiVec { + pub entries: *mut T, + pub len: usize, + pub capacity: usize, +} + +impl From> for FfiVec { + fn from(value: Vec) -> Self { + let (entries, len, capacity) = value.into_raw_parts(); + Self { + entries, + len, + capacity, + } + } +} + +impl From> for Vec { + fn from(value: FfiVec) -> Self { + unsafe { Self::from_raw_parts(value.entries, value.len, value.capacity) } + } +} + +impl FfiVec { + /// # Safety + /// + /// `index` must be lesser than `self.len`. + #[must_use] + pub unsafe fn get(&self, index: usize) -> &T { + let ptr = unsafe { self.entries.add(index) }; + unsafe { &*ptr } + } +} + +#[repr(C)] +pub struct FfiOption { + pub value: *mut T, + pub is_some: bool, +} + +impl FfiOption { + pub fn from_value(val: T) -> Self { + Self { + value: Box::into_raw(Box::new(val)), + is_some: true, + } + } + + #[must_use] + pub const fn from_none() -> Self { + Self { + value: std::ptr::null_mut(), + is_some: false, + } + } +} diff --git a/indexer/ffi/src/api/types/transaction.rs b/indexer/ffi/src/api/types/transaction.rs new file mode 100644 index 00000000..ee3bd01b --- /dev/null +++ b/indexer/ffi/src/api/types/transaction.rs @@ -0,0 +1,548 @@ +use indexer_service_protocol::{ + AccountId, Ciphertext, Commitment, CommitmentSetDigest, EncryptedAccountData, + EphemeralPublicKey, HashType, Nullifier, PrivacyPreservingMessage, + PrivacyPreservingTransaction, ProgramDeploymentMessage, ProgramDeploymentTransaction, + ProgramId, Proof, PublicKey, PublicMessage, PublicTransaction, Signature, Transaction, + ValidityWindow, WitnessSet, +}; + +use crate::api::types::{ + FfiBytes32, FfiHashType, FfiOption, FfiProgramId, FfiPublicKey, FfiSignature, FfiVec, + vectors::{ + FfiAccountIdList, FfiAccountList, FfiEncryptedAccountDataList, FfiInstructionDataList, + FfiNonceList, FfiNullifierCommitmentSetList, FfiProgramDeploymentMessage, FfiProof, + FfiSignaturePubKeyList, FfiVecBytes32, FfiVecU8, + }, +}; + +#[repr(C)] +pub struct FfiPublicTransactionBody { + pub hash: FfiHashType, + pub message: FfiPublicMessage, + pub witness_set: FfiSignaturePubKeyList, +} + +impl From for FfiPublicTransactionBody { + fn from(value: PublicTransaction) -> Self { + let PublicTransaction { + hash, + message, + witness_set, + } = value; + + Self { + hash: hash.into(), + message: message.into(), + witness_set: witness_set + .signatures_and_public_keys + .into_iter() + .map(Into::into) + .collect::>() + .into(), + } + } +} + +impl From> for PublicTransaction { + fn from(value: Box) -> Self { + Self { + hash: HashType(value.hash.data), + message: PublicMessage { + program_id: ProgramId(value.message.program_id.data), + account_ids: { + let std_vec: Vec<_> = value.message.account_ids.into(); + std_vec + .into_iter() + .map(|ffi_val| AccountId { + value: ffi_val.data, + }) + .collect() + }, + nonces: { + let std_vec: Vec<_> = value.message.nonces.into(); + std_vec.into_iter().map(Into::into).collect() + }, + instruction_data: value.message.instruction_data.into(), + }, + witness_set: WitnessSet { + signatures_and_public_keys: { + let std_vec: Vec<_> = value.witness_set.into(); + std_vec + .into_iter() + .map(|ffi_val| { + ( + Signature(ffi_val.signature.data), + PublicKey(ffi_val.public_key.data), + ) + }) + .collect() + }, + proof: None, + }, + } + } +} + +#[repr(C)] +pub struct FfiPublicMessage { + pub program_id: FfiProgramId, + pub account_ids: FfiAccountIdList, + pub nonces: FfiNonceList, + pub instruction_data: FfiInstructionDataList, +} + +impl From for FfiPublicMessage { + fn from(value: PublicMessage) -> Self { + let PublicMessage { + program_id, + account_ids, + nonces, + instruction_data, + } = value; + + Self { + program_id: program_id.into(), + account_ids: account_ids + .into_iter() + .map(Into::into) + .collect::>() + .into(), + nonces: nonces + .into_iter() + .map(Into::into) + .collect::>() + .into(), + instruction_data: instruction_data.into(), + } + } +} + +#[repr(C)] +pub struct FfiPrivateTransactionBody { + pub hash: FfiHashType, + pub message: FfiPrivacyPreservingMessage, + pub witness_set: FfiSignaturePubKeyList, + pub proof: FfiProof, +} + +impl From for FfiPrivateTransactionBody { + fn from(value: PrivacyPreservingTransaction) -> Self { + let PrivacyPreservingTransaction { + hash, + message, + witness_set, + } = value; + + Self { + hash: hash.into(), + message: message.into(), + witness_set: witness_set + .signatures_and_public_keys + .into_iter() + .map(Into::into) + .collect::>() + .into(), + proof: witness_set + .proof + .expect("Private execution: proof must be present") + .0 + .into(), + } + } +} + +impl From> for PrivacyPreservingTransaction { + fn from(value: Box) -> Self { + Self { + hash: HashType(value.hash.data), + message: PrivacyPreservingMessage { + public_account_ids: { + let std_vec: Vec<_> = value.message.public_account_ids.into(); + std_vec + .into_iter() + .map(|ffi_val| AccountId { + value: ffi_val.data, + }) + .collect() + }, + nonces: { + let std_vec: Vec<_> = value.message.nonces.into(); + std_vec.into_iter().map(Into::into).collect() + }, + public_post_states: { + let std_vec: Vec<_> = value.message.public_post_states.into(); + std_vec.into_iter().map(Into::into).collect() + }, + encrypted_private_post_states: { + let std_vec: Vec<_> = value.message.encrypted_private_post_states.into(); + std_vec + .into_iter() + .map(|ffi_val| EncryptedAccountData { + ciphertext: Ciphertext(ffi_val.ciphertext.into()), + epk: EphemeralPublicKey(ffi_val.epk.into()), + view_tag: ffi_val.view_tag, + }) + .collect() + }, + new_commitments: { + let std_vec: Vec<_> = value.message.new_commitments.into(); + std_vec + .into_iter() + .map(|ffi_val| Commitment(ffi_val.data)) + .collect() + }, + new_nullifiers: { + let std_vec: Vec<_> = value.message.new_nullifiers.into(); + std_vec + .into_iter() + .map(|ffi_val| { + ( + Nullifier(ffi_val.nullifier.data), + CommitmentSetDigest(ffi_val.commitment_set_digest.data), + ) + }) + .collect() + }, + block_validity_window: cast_ffi_validity_window( + value.message.block_validity_window, + ), + timestamp_validity_window: cast_ffi_validity_window( + value.message.timestamp_validity_window, + ), + }, + witness_set: WitnessSet { + signatures_and_public_keys: { + let std_vec: Vec<_> = value.witness_set.into(); + std_vec + .into_iter() + .map(|ffi_val| { + ( + Signature(ffi_val.signature.data), + PublicKey(ffi_val.public_key.data), + ) + }) + .collect() + }, + proof: Some(Proof(value.proof.into())), + }, + } + } +} + +#[repr(C)] +pub struct FfiPrivacyPreservingMessage { + pub public_account_ids: FfiAccountIdList, + pub nonces: FfiNonceList, + pub public_post_states: FfiAccountList, + pub encrypted_private_post_states: FfiEncryptedAccountDataList, + pub new_commitments: FfiVecBytes32, + pub new_nullifiers: FfiNullifierCommitmentSetList, + pub block_validity_window: [u64; 2], + pub timestamp_validity_window: [u64; 2], +} + +impl From for FfiPrivacyPreservingMessage { + fn from(value: PrivacyPreservingMessage) -> Self { + let PrivacyPreservingMessage { + public_account_ids, + nonces, + public_post_states, + encrypted_private_post_states, + new_commitments, + new_nullifiers, + block_validity_window, + timestamp_validity_window, + } = value; + + Self { + public_account_ids: public_account_ids + .into_iter() + .map(Into::into) + .collect::>() + .into(), + nonces: nonces + .into_iter() + .map(Into::into) + .collect::>() + .into(), + public_post_states: public_post_states + .into_iter() + .map(|acc_ind| -> nssa::Account { + acc_ind.try_into().expect("Source is in blocks, must fit") + }) + .map(Into::into) + .collect::>() + .into(), + encrypted_private_post_states: encrypted_private_post_states + .into_iter() + .map(Into::into) + .collect::>() + .into(), + new_commitments: new_commitments + .into_iter() + .map(|comm| FfiBytes32 { data: comm.0 }) + .collect::>() + .into(), + new_nullifiers: new_nullifiers + .into_iter() + .map(Into::into) + .collect::>() + .into(), + block_validity_window: cast_validity_window(block_validity_window), + timestamp_validity_window: cast_validity_window(timestamp_validity_window), + } + } +} + +#[repr(C)] +pub struct FfiNullifierCommitmentSet { + pub nullifier: FfiBytes32, + pub commitment_set_digest: FfiBytes32, +} + +impl From<(Nullifier, CommitmentSetDigest)> for FfiNullifierCommitmentSet { + fn from(value: (Nullifier, CommitmentSetDigest)) -> Self { + Self { + nullifier: FfiBytes32 { data: value.0.0 }, + commitment_set_digest: FfiBytes32 { data: value.1.0 }, + } + } +} + +#[repr(C)] +pub struct FfiEncryptedAccountData { + pub ciphertext: FfiVecU8, + pub epk: FfiVecU8, + pub view_tag: u8, +} + +impl From for FfiEncryptedAccountData { + fn from(value: EncryptedAccountData) -> Self { + let EncryptedAccountData { + ciphertext, + epk, + view_tag, + } = value; + + Self { + ciphertext: ciphertext.0.into(), + epk: epk.0.into(), + view_tag, + } + } +} + +#[repr(C)] +pub struct FfiSignaturePubKeyEntry { + pub signature: FfiSignature, + pub public_key: FfiPublicKey, +} + +impl From<(Signature, PublicKey)> for FfiSignaturePubKeyEntry { + fn from(value: (Signature, PublicKey)) -> Self { + Self { + signature: value.0.into(), + public_key: value.1.into(), + } + } +} + +#[repr(C)] +pub struct FfiProgramDeploymentTransactionBody { + pub hash: FfiHashType, + pub message: FfiProgramDeploymentMessage, +} + +impl From> for ProgramDeploymentTransaction { + fn from(value: Box) -> Self { + Self { + hash: HashType(value.hash.data), + message: ProgramDeploymentMessage { + bytecode: value.message.into(), + }, + } + } +} + +impl From for FfiProgramDeploymentTransactionBody { + fn from(value: ProgramDeploymentTransaction) -> Self { + let ProgramDeploymentTransaction { hash, message } = value; + + Self { + hash: hash.into(), + message: message.bytecode.into(), + } + } +} + +#[repr(C)] +pub struct FfiTransactionBody { + pub public_body: *mut FfiPublicTransactionBody, + pub private_body: *mut FfiPrivateTransactionBody, + pub program_deployment_body: *mut FfiProgramDeploymentTransactionBody, +} + +#[repr(C)] +pub struct FfiTransaction { + pub body: FfiTransactionBody, + pub kind: FfiTransactionKind, +} + +impl From for FfiTransaction { + fn from(value: Transaction) -> Self { + match value { + Transaction::Public(pub_tx) => Self { + body: FfiTransactionBody { + public_body: Box::into_raw(Box::new(pub_tx.into())), + private_body: std::ptr::null_mut(), + program_deployment_body: std::ptr::null_mut(), + }, + kind: FfiTransactionKind::Public, + }, + Transaction::PrivacyPreserving(priv_tx) => Self { + body: FfiTransactionBody { + public_body: std::ptr::null_mut(), + private_body: Box::into_raw(Box::new(priv_tx.into())), + program_deployment_body: std::ptr::null_mut(), + }, + kind: FfiTransactionKind::Private, + }, + Transaction::ProgramDeployment(pr_dep_tx) => Self { + body: FfiTransactionBody { + public_body: std::ptr::null_mut(), + private_body: std::ptr::null_mut(), + program_deployment_body: Box::into_raw(Box::new(pr_dep_tx.into())), + }, + kind: FfiTransactionKind::ProgramDeploy, + }, + } + } +} + +#[repr(C)] +pub enum FfiTransactionKind { + Public = 0x0, + Private, + ProgramDeploy, +} + +/// Frees the resources associated with the given ffi transaction. +/// +/// # Arguments +/// +/// - `val`: An instance of `FfiTransaction`. +/// +/// # Returns +/// +/// void. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `val` is a valid instance of `FfiTransaction`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn free_ffi_transaction(val: FfiTransaction) { + match val.kind { + FfiTransactionKind::Public => { + let body = unsafe { Box::from_raw(val.body.public_body) }; + let std_body: PublicTransaction = body.into(); + drop(std_body); + } + FfiTransactionKind::Private => { + let body = unsafe { Box::from_raw(val.body.private_body) }; + let std_body: PrivacyPreservingTransaction = body.into(); + drop(std_body); + } + FfiTransactionKind::ProgramDeploy => { + let body = unsafe { Box::from_raw(val.body.program_deployment_body) }; + let std_body: ProgramDeploymentTransaction = body.into(); + drop(std_body); + } + } +} + +/// Frees the resources associated with the given ffi transaction option. +/// +/// # Arguments +/// +/// - `val`: An instance of `FfiOption`. +/// +/// # Returns +/// +/// void. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `val` is a valid instance of `FfiOption`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn free_ffi_transaction_opt(val: FfiOption) { + if val.is_some { + let value = unsafe { Box::from_raw(val.value) }; + + match value.kind { + FfiTransactionKind::Public => { + let body = unsafe { Box::from_raw(value.body.public_body) }; + let std_body: PublicTransaction = body.into(); + drop(std_body); + } + FfiTransactionKind::Private => { + let body = unsafe { Box::from_raw(value.body.private_body) }; + let std_body: PrivacyPreservingTransaction = body.into(); + drop(std_body); + } + FfiTransactionKind::ProgramDeploy => { + let body = unsafe { Box::from_raw(value.body.program_deployment_body) }; + let std_body: ProgramDeploymentTransaction = body.into(); + drop(std_body); + } + } + } +} + +/// Frees the resources associated with the given vector of ffi transactions. +/// +/// # Arguments +/// +/// - `val`: An instance of `FfiVec`. +/// +/// # Returns +/// +/// void. +/// +/// # Safety +/// +/// The caller must ensure that: +/// - `val` is a valid instance of `FfiVec`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn free_ffi_transaction_vec(val: FfiVec) { + let ffi_tx_std_vec: Vec<_> = val.into(); + for tx in ffi_tx_std_vec { + unsafe { + free_ffi_transaction(tx); + } + } +} + +fn cast_validity_window(window: ValidityWindow) -> [u64; 2] { + [ + window.0.0.unwrap_or_default(), + window.0.1.unwrap_or(u64::MAX), + ] +} + +const fn cast_ffi_validity_window(ffi_window: [u64; 2]) -> ValidityWindow { + let left = if ffi_window[0] == 0 { + None + } else { + Some(ffi_window[0]) + }; + + let right = if ffi_window[1] == u64::MAX { + None + } else { + Some(ffi_window[1]) + }; + + ValidityWindow((left, right)) +} diff --git a/indexer/ffi/src/api/types/vectors.rs b/indexer/ffi/src/api/types/vectors.rs new file mode 100644 index 00000000..46f08737 --- /dev/null +++ b/indexer/ffi/src/api/types/vectors.rs @@ -0,0 +1,31 @@ +use crate::api::types::{ + FfiAccountId, FfiBytes32, FfiNonce, FfiVec, + account::FfiAccount, + transaction::{ + FfiEncryptedAccountData, FfiNullifierCommitmentSet, FfiSignaturePubKeyEntry, FfiTransaction, + }, +}; + +pub type FfiVecU8 = FfiVec; + +pub type FfiAccountList = FfiVec; + +pub type FfiAccountIdList = FfiVec; + +pub type FfiVecBytes32 = FfiVec; + +pub type FfiBlockBody = FfiVec; + +pub type FfiNonceList = FfiVec; + +pub type FfiInstructionDataList = FfiVec; + +pub type FfiSignaturePubKeyList = FfiVec; + +pub type FfiProof = FfiVecU8; + +pub type FfiProgramDeploymentMessage = FfiVecU8; + +pub type FfiEncryptedAccountDataList = FfiVec; + +pub type FfiNullifierCommitmentSetList = FfiVec; diff --git a/indexer/ffi/src/client.rs b/indexer/ffi/src/client.rs new file mode 100644 index 00000000..f05b350e --- /dev/null +++ b/indexer/ffi/src/client.rs @@ -0,0 +1,33 @@ +use std::{ops::Deref, sync::Arc}; + +use anyhow::{Context as _, Result}; +use log::info; +pub use url::Url; + +pub trait IndexerClientTrait: Clone { + async fn new(indexer_url: &Url) -> Result; +} + +#[derive(Clone)] +pub struct IndexerClient(Arc); + +impl IndexerClientTrait for IndexerClient { + async fn new(indexer_url: &Url) -> Result { + info!("Connecting to Indexer at {indexer_url}"); + + let client = jsonrpsee::ws_client::WsClientBuilder::default() + .build(indexer_url) + .await + .context("Failed to create websocket client")?; + + Ok(Self(Arc::new(client))) + } +} + +impl Deref for IndexerClient { + type Target = jsonrpsee::ws_client::WsClient; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/indexer_ffi/src/errors.rs b/indexer/ffi/src/errors.rs similarity index 94% rename from indexer_ffi/src/errors.rs rename to indexer/ffi/src/errors.rs index 46aa0f9f..4572474c 100644 --- a/indexer_ffi/src/errors.rs +++ b/indexer/ffi/src/errors.rs @@ -5,6 +5,7 @@ pub enum OperationStatus { Ok = 0x0, NullPointer = 0x1, InitializationError = 0x2, + ClientError = 0x3, } impl OperationStatus { diff --git a/indexer_ffi/src/indexer.rs b/indexer/ffi/src/indexer.rs similarity index 59% rename from indexer_ffi/src/indexer.rs rename to indexer/ffi/src/indexer.rs index c110b183..33800356 100644 --- a/indexer_ffi/src/indexer.rs +++ b/indexer/ffi/src/indexer.rs @@ -3,18 +3,26 @@ use std::{ffi::c_void, net::SocketAddr}; use indexer_service::IndexerHandle; use tokio::runtime::Runtime; +use crate::client::IndexerClient; + #[repr(C)] pub struct IndexerServiceFFI { indexer_handle: *mut c_void, runtime: *mut c_void, + indexer_client: *mut c_void, } impl IndexerServiceFFI { - pub fn new(indexer_handle: indexer_service::IndexerHandle, runtime: Runtime) -> Self { + pub fn new( + indexer_handle: indexer_service::IndexerHandle, + runtime: Runtime, + indexer_client: IndexerClient, + ) -> Self { Self { // Box the complex types and convert to opaque pointers indexer_handle: Box::into_raw(Box::new(indexer_handle)).cast::(), runtime: Box::into_raw(Box::new(runtime)).cast::(), + indexer_client: Box::into_raw(Box::new(indexer_client)).cast::(), } } @@ -25,10 +33,11 @@ impl IndexerServiceFFI { /// The caller must ensure that: /// - `self` is a valid object(contains valid pointers in all fields) #[must_use] - pub unsafe fn into_parts(self) -> (Box, Box) { + pub unsafe fn into_parts(self) -> (Box, Box, Box) { let indexer_handle = unsafe { Box::from_raw(self.indexer_handle.cast::()) }; let runtime = unsafe { Box::from_raw(self.runtime.cast::()) }; - (indexer_handle, runtime) + let indexer_client = unsafe { Box::from_raw(self.indexer_client.cast::()) }; + (indexer_handle, runtime, indexer_client) } /// Helper to get indexer handle addr. @@ -49,7 +58,7 @@ impl IndexerServiceFFI { indexer_handle.addr() } - /// Helper to get indexer handle addr. + /// Helper to get indexer handle ref. /// /// # Safety /// @@ -64,6 +73,38 @@ impl IndexerServiceFFI { .expect("Indexer Handle must be non-null pointer") } } + + /// Helper to get indexer client ref. + /// + /// # Safety + /// + /// The caller must ensure that: + /// - `self` is a valid object(contains valid pointers in all fields) + #[must_use] + pub const unsafe fn client(&self) -> &IndexerClient { + unsafe { + self.indexer_client + .cast::() + .as_ref() + .expect("Indexer Client must be non-null pointer") + } + } + + /// Helper to get indexer runtime ref. + /// + /// # Safety + /// + /// The caller must ensure that: + /// - `self` is a valid object(contains valid pointers in all fields) + #[must_use] + pub const unsafe fn runtime(&self) -> &Runtime { + unsafe { + self.runtime + .cast::() + .as_ref() + .expect("Indexer Runtime must be non-null pointer") + } + } } // Implement Drop to prevent memory leaks @@ -72,6 +113,7 @@ impl Drop for IndexerServiceFFI { let Self { indexer_handle, runtime, + indexer_client, } = self; if indexer_handle.is_null() { @@ -80,7 +122,11 @@ impl Drop for IndexerServiceFFI { if runtime.is_null() { log::error!("Attempted to drop a null tokio runtime pointer. This is a bug"); } + if indexer_client.is_null() { + log::error!("Attempted to drop a null client pointer. This is a bug"); + } drop(unsafe { Box::from_raw(indexer_handle.cast::()) }); drop(unsafe { Box::from_raw(runtime.cast::()) }); + drop(unsafe { Box::from_raw(indexer_client.cast::()) }); } } diff --git a/indexer_ffi/src/lib.rs b/indexer/ffi/src/lib.rs similarity index 93% rename from indexer_ffi/src/lib.rs rename to indexer/ffi/src/lib.rs index fe594ec0..5806a074 100644 --- a/indexer_ffi/src/lib.rs +++ b/indexer/ffi/src/lib.rs @@ -4,5 +4,6 @@ pub use errors::OperationStatus; pub use indexer::IndexerServiceFFI; pub mod api; +mod client; mod errors; mod indexer; diff --git a/indexer_ffi/indexer_ffi.h b/indexer_ffi/indexer_ffi.h deleted file mode 100644 index 7c7d9a4d..00000000 --- a/indexer_ffi/indexer_ffi.h +++ /dev/null @@ -1,76 +0,0 @@ -#include -#include -#include -#include - -typedef enum OperationStatus { - Ok = 0, - NullPointer = 1, - InitializationError = 2, -} OperationStatus; - -typedef struct IndexerServiceFFI { - void *indexer_handle; - void *runtime; -} IndexerServiceFFI; - -/** - * 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_IndexerServiceFFI__OperationStatus { - struct IndexerServiceFFI *value; - enum OperationStatus error; -} PointerResult_IndexerServiceFFI__OperationStatus; - -typedef struct PointerResult_IndexerServiceFFI__OperationStatus InitializedIndexerServiceFFIResult; - -/** - * Creates and starts an indexer based on the provided - * configuration file path. - * - * # Arguments - * - * - `config_path`: A pointer to a string representing the path to the configuration file. - * - `port`: Number representing a port, on which indexers RPC will start. - * - * # Returns - * - * An `InitializedIndexerServiceFFIResult` containing either a pointer to the - * initialized `IndexerServiceFFI` or an error code. - */ -InitializedIndexerServiceFFIResult start_indexer(const char *config_path, uint16_t port); - -/** - * Stops and frees the resources associated with the given indexer service. - * - * # Arguments - * - * - `indexer`: A pointer to the `IndexerServiceFFI` instance to be stopped. - * - * # Returns - * - * An `OperationStatus` indicating success or failure. - * - * # Safety - * - * The caller must ensure that: - * - `indexer` is a valid pointer to a `IndexerServiceFFI` instance - * - The `IndexerServiceFFI` instance was created by this library - * - The pointer will not be used after this function returns - */ -enum OperationStatus stop_indexer(struct IndexerServiceFFI *indexer); - -/** - * # Safety - * It's up to the caller to pass a proper pointer, if somehow from c/c++ side - * this is called with a type which doesn't come from a returned `CString` it - * will cause a segfault. - */ -void free_cstring(char *block); - -bool is_ok(const enum OperationStatus *self); - -bool is_error(const enum OperationStatus *self); diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index a1704cdf..5f1f1037 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -25,6 +25,7 @@ jsonrpsee = { workspace = true, features = ["ws-client"] } wallet-ffi.workspace = true indexer_ffi.workspace = true testnet_initial_state.workspace = true +indexer_service_protocol.workspace = true url.workspace = true diff --git a/integration_tests/src/test_context_ffi.rs b/integration_tests/src/test_context_ffi.rs index b29ace7c..d03a4e00 100644 --- a/integration_tests/src/test_context_ffi.rs +++ b/integration_tests/src/test_context_ffi.rs @@ -266,6 +266,11 @@ impl BlockingTestContextFFI { pub fn runtime_clone(&self) -> Arc { Arc::::clone(&self.runtime) } + + #[must_use] + pub const fn indexer_ffi(&self) -> *const IndexerServiceFFI { + &raw const (self.indexer_ffi) + } } impl Drop for BlockingTestContextFFI { diff --git a/integration_tests/tests/indexer_ffi.rs b/integration_tests/tests/indexer_ffi.rs index ac1dab4c..bbc329e3 100644 --- a/integration_tests/tests/indexer_ffi.rs +++ b/integration_tests/tests/indexer_ffi.rs @@ -1,11 +1,18 @@ #![expect( clippy::shadow_unrelated, clippy::tests_outside_test_module, + clippy::undocumented_unsafe_blocks, reason = "We don't care about these in tests" )] use anyhow::{Context as _, Result}; -use indexer_service_rpc::RpcClient as _; +use indexer_ffi::{ + IndexerServiceFFI, OperationStatus, + api::{ + PointerResult, + types::{FfiAccountId, FfiOption, FfiVec, account::FfiAccount, block::FfiBlock}, + }, +}; use integration_tests::{ TIME_TO_WAIT_FOR_BLOCK_SECONDS, format_private_account_id, format_public_account_id, test_context_ffi::BlockingTestContextFFI, verify_commitment_is_in_state, @@ -17,6 +24,23 @@ use wallet::cli::{Command, programs::native_token_transfer::AuthTransferSubcomma /// Maximum time to wait for the indexer to catch up to the sequencer. const L2_TO_L1_TIMEOUT_MILLIS: u64 = 180_000; +unsafe extern "C" { + unsafe fn query_last_block( + indexer: *const IndexerServiceFFI, + ) -> PointerResult; + + unsafe fn query_block_vec( + indexer: *const IndexerServiceFFI, + before: FfiOption, + limit: u64, + ) -> PointerResult, OperationStatus>; + + unsafe fn query_account( + indexer: *const IndexerServiceFFI, + account_id: FfiAccountId, + ) -> PointerResult; +} + #[test] fn indexer_test_run_ffi() -> Result<()> { let blocking_ctx = BlockingTestContextFFI::new()?; @@ -28,10 +52,19 @@ fn indexer_test_run_ffi() -> Result<()> { }); let last_block_indexer = blocking_ctx.ctx().get_last_block_indexer(runtime_wrapped)?; + let last_block_indexer_ffi_res = unsafe { query_last_block(blocking_ctx.indexer_ffi()) }; + + assert!(last_block_indexer_ffi_res.error.is_ok()); + + let last_block_indexer_ffi = unsafe { *last_block_indexer_ffi_res.value }; info!("Last block on ind now is {last_block_indexer}"); + info!("Last block on ind ffi now is {last_block_indexer_ffi}"); assert!(last_block_indexer > 1); + assert!(last_block_indexer_ffi > 1); + + assert_eq!(last_block_indexer, last_block_indexer_ffi); Ok(()) } @@ -40,7 +73,6 @@ fn indexer_test_run_ffi() -> Result<()> { fn indexer_ffi_block_batching() -> Result<()> { let blocking_ctx = BlockingTestContextFFI::new()?; let runtime_wrapped = blocking_ctx.runtime(); - let ctx = blocking_ctx.ctx(); // WAIT info!("Waiting for indexer to parse blocks"); @@ -48,31 +80,36 @@ fn indexer_ffi_block_batching() -> Result<()> { tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await; }); - let last_block_indexer = runtime_wrapped - .block_on(ctx.indexer_client().get_last_finalized_block_id()) - .unwrap(); + let last_block_indexer_ffi_res = unsafe { query_last_block(blocking_ctx.indexer_ffi()) }; + + assert!(last_block_indexer_ffi_res.error.is_ok()); + + let last_block_indexer = unsafe { *last_block_indexer_ffi_res.value }; info!("Last block on ind now is {last_block_indexer}"); assert!(last_block_indexer > 1); - // Getting wide batch to fit all blocks (from latest backwards) - let mut block_batch = runtime_wrapped - .block_on(ctx.indexer_client().get_blocks(None, 100)) - .unwrap(); + let before_ffi = FfiOption::::from_none(); + let limit = 100; - // Reverse to check chain consistency from oldest to newest - block_batch.reverse(); + let block_batch_ffi_res = + unsafe { query_block_vec(blocking_ctx.indexer_ffi(), before_ffi, limit) }; - // Checking chain consistency - let mut prev_block_hash = block_batch.first().unwrap().header.hash; + assert!(block_batch_ffi_res.error.is_ok()); - for block in &block_batch[1..] { - assert_eq!(block.header.prev_block_hash, prev_block_hash); + let block_batch = unsafe { &*block_batch_ffi_res.value }; + + let mut last_block_prev_hash = unsafe { block_batch.get(0) }.header.prev_block_hash.data; + + for i in 1..block_batch.len { + let block = unsafe { block_batch.get(i) }; + + assert_eq!(last_block_prev_hash, block.header.hash.data); info!("Block {} chain-consistent", block.header.block_id); - prev_block_hash = block.header.hash; + last_block_prev_hash = block.header.prev_block_hash.data; } Ok(()) @@ -82,6 +119,7 @@ fn indexer_ffi_block_batching() -> Result<()> { fn indexer_ffi_state_consistency() -> Result<()> { let mut blocking_ctx = BlockingTestContextFFI::new()?; let runtime_wrapped = blocking_ctx.runtime_clone(); + let indexer_ffi = blocking_ctx.indexer_ffi(); let ctx = blocking_ctx.ctx_mut(); let command = Command::AuthTransfer(AuthTransferSubcommand::Send { @@ -175,14 +213,21 @@ fn indexer_ffi_state_consistency() -> Result<()> { tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await; }); - let acc1_ind_state = runtime_wrapped.block_on( - ctx.indexer_client() - .get_account(ctx.existing_public_accounts()[0].into()), - )?; - let acc2_ind_state = runtime_wrapped.block_on( - ctx.indexer_client() - .get_account(ctx.existing_public_accounts()[1].into()), - )?; + let acc1_ind_state_ffi = + unsafe { query_account(indexer_ffi, (&ctx.existing_public_accounts()[0]).into()) }; + + assert!(acc1_ind_state_ffi.error.is_ok()); + + let acc1_ind_state_pre = unsafe { &*acc1_ind_state_ffi.value }; + let acc1_ind_state: indexer_service_protocol::Account = acc1_ind_state_pre.into(); + + let acc2_ind_state_ffi = + unsafe { query_account(indexer_ffi, (&ctx.existing_public_accounts()[1]).into()) }; + + assert!(acc2_ind_state_ffi.error.is_ok()); + + let acc2_ind_state_pre = unsafe { &*acc2_ind_state_ffi.value }; + let acc2_ind_state: indexer_service_protocol::Account = acc2_ind_state_pre.into(); info!("Checking correct state transition"); let acc1_seq_state = @@ -208,6 +253,7 @@ fn indexer_ffi_state_consistency() -> Result<()> { fn indexer_ffi_state_consistency_with_labels() -> Result<()> { let mut blocking_ctx = BlockingTestContextFFI::new()?; let runtime_wrapped = blocking_ctx.runtime_clone(); + let indexer_ffi = blocking_ctx.indexer_ffi(); let ctx = blocking_ctx.ctx_mut(); // Assign labels to both accounts @@ -269,10 +315,14 @@ fn indexer_ffi_state_consistency_with_labels() -> Result<()> { tokio::time::sleep(std::time::Duration::from_millis(L2_TO_L1_TIMEOUT_MILLIS)).await; }); - let acc1_ind_state = runtime_wrapped.block_on( - ctx.indexer_client() - .get_account(ctx.existing_public_accounts()[0].into()), - )?; + let acc1_ind_state_ffi = + unsafe { query_account(indexer_ffi, (&ctx.existing_public_accounts()[0]).into()) }; + + assert!(acc1_ind_state_ffi.error.is_ok()); + + let acc1_ind_state_pre = unsafe { &*acc1_ind_state_ffi.value }; + let acc1_ind_state: indexer_service_protocol::Account = acc1_ind_state_pre.into(); + let acc1_seq_state = runtime_wrapped.block_on(sequencer_service_rpc::RpcClient::get_account( ctx.sequencer_client(), diff --git a/wallet-ffi/Cargo.toml b/wallet-ffi/Cargo.toml index 0af20a54..37d552ec 100644 --- a/wallet-ffi/Cargo.toml +++ b/wallet-ffi/Cargo.toml @@ -15,6 +15,7 @@ wallet.workspace = true nssa.workspace = true nssa_core.workspace = true sequencer_service_rpc = { workspace = true, features = ["client"] } + tokio.workspace = true [build-dependencies]