From fe9dee057399fc961c038222dea0cf7e39caca03 Mon Sep 17 00:00:00 2001 From: Sergiu Popescu <44298302+sergiupopescu199@users.noreply.github.com> Date: Mon, 23 Sep 2024 09:08:59 +0200 Subject: [PATCH 01/24] feat(iota-indexer): add read_api rpc tests (#2552) * feat(iota-indexer): add read_api rpc tests * fixup! feat(iota-indexer): add read_api rpc tests * fix(iota-indexer): respect show_raw_effects option * fix(iota-json-rpc): respect show_raw_effects option * fix(iota-indexer): add an assertion to test node rpc options * refactor(iota-indexer): handle options in a more idiomatic way * fixup! refactor(iota-indexer): handle options in a more idiomatic way * fix(iota-indexer): Fix README * fix(iota-indexer): add serial_test for all test cases refactor(iota-indexer): add common module for tests * fixup! fix(iota-indexer): add serial_test for all test cases * fixup! fix(iota-indexer): add serial_test for all test cases * fixup! fixup! fix(iota-indexer): add serial_test for all test cases * fixup! fixup! fixup! fix(iota-indexer): add serial_test for all test cases * fixup! fixup! fixup! fixup! fix(iota-indexer): add serial_test for all test cases --- Cargo.lock | 3 +- crates/iota-indexer/Cargo.toml | 5 +- crates/iota-indexer/README.md | 22 +- .../iota-indexer/src/models/transactions.rs | 124 +- crates/iota-indexer/tests/common/mod.rs | 161 +++ crates/iota-indexer/tests/ingestion_tests.rs | 78 +- crates/iota-indexer/tests/rpc-tests/main.rs | 9 + .../iota-indexer/tests/rpc-tests/read_api.rs | 1215 +++++++++++++++++ crates/iota-json-rpc/src/read_api.rs | 12 + 9 files changed, 1491 insertions(+), 138 deletions(-) create mode 100644 crates/iota-indexer/tests/common/mod.rs create mode 100644 crates/iota-indexer/tests/rpc-tests/main.rs create mode 100644 crates/iota-indexer/tests/rpc-tests/read_api.rs diff --git a/Cargo.lock b/Cargo.lock index 0588e0451a2..a10e5d59cd4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6003,7 +6003,6 @@ dependencies = [ "backoff", "bcs", "cached", - "chrono", "clap", "diesel", "diesel_migrations", @@ -6036,9 +6035,11 @@ dependencies = [ "serde", "serde_json", "serde_with", + "serial_test", "simulacrum", "tap", "telemetry-subscribers", + "test-cluster", "thiserror", "tokio", "tracing", diff --git a/crates/iota-indexer/Cargo.toml b/crates/iota-indexer/Cargo.toml index 49ce352a1e3..94d86b9e788 100644 --- a/crates/iota-indexer/Cargo.toml +++ b/crates/iota-indexer/Cargo.toml @@ -12,7 +12,6 @@ async-trait.workspace = true axum.workspace = true backoff.workspace = true bcs.workspace = true -chrono.workspace = true clap.workspace = true diesel.workspace = true futures.workspace = true @@ -21,7 +20,6 @@ jsonrpsee.workspace = true prometheus.workspace = true regex.workspace = true serde.workspace = true -serde_json.workspace = true serde_with.workspace = true tap.workspace = true thiserror.workspace = true @@ -60,7 +58,10 @@ iota-keys.workspace = true iota-move-build.workspace = true iota-swarm-config.workspace = true rand.workspace = true +serde_json.workspace = true +serial_test = "2.0" simulacrum.workspace = true +test-cluster.workspace = true [[bin]] name = "iota-indexer" diff --git a/crates/iota-indexer/README.md b/crates/iota-indexer/README.md index 4e359324097..f6341f80bc9 100644 --- a/crates/iota-indexer/README.md +++ b/crates/iota-indexer/README.md @@ -84,12 +84,30 @@ diesel database reset --database-url="postgres://postgres:postgrespw@localhost/i ### Running tests -To run the tests, a running postgres instance is required. The crate provides following tests currently: +To run the tests, a running postgres instance is required. + +```sh +docker run --name iota-indexer-tests -e POSTGRES_PASSWORD=postgrespw -e POSTGRES_USER=postgres -e POSTGRES_DB=iota_indexer -d -p 5432:5432 postgres +``` + +The crate provides following tests currently: - unit tests for DB models (objects, events) which test the conversion between the database representation and the Rust representation of the objects and events. - unit tests for the DB query filters, which test the conversion of filters to the correct SQL queries. - integration tests (see [ingestion_tests](tests/ingestion_tests.rs)) to make sure the indexer correctly indexes transaction data from a full node by comparing the data in the database with the data received from the fullnode. +- rpc tests (see [rpc-tests](tests/rpc-tests/main.rs)) + +> [!NOTE] +> rpc tests which relies on postgres for every test it applies migrations, we need to run tests sequencially to avoid errors + +```sh +# run integration tests & rpc tests +cargo test --features pg_integration -- --test-threads 1 +``` + +For a better testing experience is possible to use [nextest](https://nexte.st/) ```sh -cargo test --features pg_integration +# run integration tests & rpc tests +cargo nextest run --features pg_integration --test-threads 1 ``` diff --git a/crates/iota-indexer/src/models/transactions.rs b/crates/iota-indexer/src/models/transactions.rs index 2ba566082c6..9d7e6382a1c 100644 --- a/crates/iota-indexer/src/models/transactions.rs +++ b/crates/iota-indexer/src/models/transactions.rs @@ -118,62 +118,59 @@ impl StoredTransaction { )) })?; - let transaction = if options.show_input { - let sender_signed_data = self.try_into_sender_signed_data()?; - let tx_block = IotaTransactionBlock::try_from(sender_signed_data, module)?; - Some(tx_block) - } else { - None - }; - - let effects = if options.show_effects { - let effects = self.try_into_iota_transaction_effects()?; - Some(effects) - } else { - None - }; - - let raw_transaction = if options.show_raw_input { - self.raw_transaction - } else { - Vec::new() - }; - - let events = if options.show_events { - let events = self - .events - .into_iter() - .map(|event| match event { - Some(event) => { - let event: Event = bcs::from_bytes(&event).map_err(|e| { + let transaction = options + .show_input + .then(|| { + let sender_signed_data = self.try_into_sender_signed_data()?; + IotaTransactionBlock::try_from(sender_signed_data, module) + }) + .transpose()?; + + let effects = options + .show_effects + .then(|| self.try_into_iota_transaction_effects()) + .transpose()?; + + let raw_transaction = options + .show_raw_input + .then_some(self.raw_transaction) + .unwrap_or_default(); + + let events = options + .show_events + .then(|| { + let events = + self.events + .into_iter() + .map(|event| match event { + Some(event) => { + let event: Event = bcs::from_bytes(&event).map_err(|e| { IndexerError::PersistentStorageDataCorruptionError(format!( "Can't convert event bytes into Event. tx_digest={:?} Error: {e}", tx_digest )) })?; - Ok(event) - } - None => Err(IndexerError::PersistentStorageDataCorruptionError(format!( - "Event should not be null, tx_digest={:?}", - tx_digest - ))), - }) - .collect::, IndexerError>>()?; - let timestamp = self.timestamp_ms as u64; - let tx_events = TransactionEvents { data: events }; - let tx_events = IotaTransactionBlockEvents::try_from_using_module_resolver( - tx_events, - tx_digest, - Some(timestamp), - module, - )?; - Some(tx_events) - } else { - None - }; - - let object_changes = if options.show_object_changes { - let object_changes = self.object_changes.into_iter().map(|object_change| { + Ok(event) + } + None => Err(IndexerError::PersistentStorageDataCorruptionError( + format!("Event should not be null, tx_digest={:?}", tx_digest), + )), + }) + .collect::, IndexerError>>()?; + let timestamp = self.timestamp_ms as u64; + let tx_events = TransactionEvents { data: events }; + IotaTransactionBlockEvents::try_from_using_module_resolver( + tx_events, + tx_digest, + Some(timestamp), + module, + ) + .map_err(Into::::into) + }) + .transpose()?; + + let object_changes = options.show_object_changes.then(|| + self.object_changes.into_iter().map(|object_change| { match object_change { Some(object_change) => { let object_change: IndexedObjectChange = bcs::from_bytes(&object_change) @@ -184,15 +181,11 @@ impl StoredTransaction { } None => Err(IndexerError::PersistentStorageDataCorruptionError(format!("object_change should not be null, tx_digest={:?}", tx_digest))), } - }).collect::, IndexerError>>()?; - - Some(object_changes) - } else { - None - }; + }).collect::, IndexerError>>() + ).transpose()?; - let balance_changes = if options.show_balance_changes { - let balance_changes = self.balance_changes.into_iter().map(|balance_change| { + let balance_changes = options.show_balance_changes.then(|| + self.balance_changes.into_iter().map(|balance_change| { match balance_change { Some(balance_change) => { let balance_change: BalanceChange = bcs::from_bytes(&balance_change) @@ -203,12 +196,13 @@ impl StoredTransaction { } None => Err(IndexerError::PersistentStorageDataCorruptionError(format!("object_change should not be null, tx_digest={:?}", tx_digest))), } - }).collect::, IndexerError>>()?; + }).collect::, IndexerError>>() + ).transpose()?; - Some(balance_changes) - } else { - None - }; + let raw_effects = options + .show_raw_effects + .then_some(self.raw_effects) + .unwrap_or_default(); Ok(IotaTransactionBlockResponse { digest: tx_digest, @@ -222,7 +216,7 @@ impl StoredTransaction { checkpoint: Some(self.checkpoint_sequence_number as u64), confirmed_local_execution: None, errors: vec![], - raw_effects: self.raw_effects, + raw_effects, }) } diff --git a/crates/iota-indexer/tests/common/mod.rs b/crates/iota-indexer/tests/common/mod.rs new file mode 100644 index 00000000000..158c434902f --- /dev/null +++ b/crates/iota-indexer/tests/common/mod.rs @@ -0,0 +1,161 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +#[allow(dead_code)] +#[cfg(feature = "pg_integration")] +pub mod pg_integration { + use std::{net::SocketAddr, sync::Arc, time::Duration}; + + use iota_config::node::RunWithRange; + use iota_indexer::{ + errors::IndexerError, + indexer::Indexer, + store::{indexer_store::IndexerStore, PgIndexerStore}, + test_utils::{start_test_indexer, ReaderWriterConfig}, + IndexerConfig, + }; + use iota_metrics::init_metrics; + use iota_types::storage::ReadStore; + use jsonrpsee::{ + http_client::{HttpClient, HttpClientBuilder}, + types::ErrorObject, + }; + use simulacrum::Simulacrum; + use test_cluster::{TestCluster, TestClusterBuilder}; + use tokio::task::JoinHandle; + + const DEFAULT_DB_URL: &str = "postgres://postgres:postgrespw@localhost:5432/iota_indexer"; + const DEFAULT_INDEXER_IP: &str = "127.0.0.1"; + const DEFAULT_INDEXER_PORT: u16 = 9005; + const DEFAULT_SERVER_PORT: u16 = 3000; + + /// Start a [`TestCluster`][`test_cluster::TestCluster`] with a `Read` & + /// `Write` indexer + pub async fn start_test_cluster_with_read_write_indexer( + stop_cluster_after_checkpoint_seq: Option, + ) -> (TestCluster, PgIndexerStore, HttpClient) { + let mut builder = TestClusterBuilder::new(); + + // run the cluster until the declared checkpoint sequence number + if let Some(stop_cluster_after_checkpoint_seq) = stop_cluster_after_checkpoint_seq { + builder = builder.with_fullnode_run_with_range(Some(RunWithRange::Checkpoint( + stop_cluster_after_checkpoint_seq, + ))); + }; + + let cluster = builder.build().await; + + // start indexer in write mode + let (pg_store, _pg_store_handle) = start_test_indexer( + Some(DEFAULT_DB_URL.to_owned()), + cluster.rpc_url().to_string(), + ReaderWriterConfig::writer_mode(None), + ) + .await; + + // start indexer in read mode + start_indexer_reader(cluster.rpc_url().to_owned()); + + // create an RPC client by using the indexer url + let rpc_client = HttpClientBuilder::default() + .build(format!( + "http://{DEFAULT_INDEXER_IP}:{DEFAULT_INDEXER_PORT}" + )) + .unwrap(); + + (cluster, pg_store, rpc_client) + } + + /// Wait for the indexer to catch up to the given checkpoint sequence number + /// + /// Indexer starts storing data after checkpoint 0 + pub async fn indexer_wait_for_checkpoint( + pg_store: &PgIndexerStore, + checkpoint_sequence_number: u64, + ) { + tokio::time::timeout(Duration::from_secs(10), async { + while { + let cp_opt = pg_store + .get_latest_tx_checkpoint_sequence_number() + .await + .unwrap(); + cp_opt.is_none() || (cp_opt.unwrap() < checkpoint_sequence_number) + } { + tokio::time::sleep(Duration::from_secs(1)).await; + } + }) + .await + .expect("Timeout waiting for indexer to catchup to checkpoint"); + } + + /// Start an Indexer instance in `Read` mode + fn start_indexer_reader(fullnode_rpc_url: impl Into) { + let config = IndexerConfig { + db_url: Some(DEFAULT_DB_URL.to_owned()), + rpc_client_url: fullnode_rpc_url.into(), + reset_db: true, + rpc_server_worker: true, + rpc_server_url: DEFAULT_INDEXER_IP.to_owned(), + rpc_server_port: DEFAULT_INDEXER_PORT, + ..Default::default() + }; + + let registry = prometheus::Registry::default(); + init_metrics(®istry); + + tokio::spawn(async move { + Indexer::start_reader(&config, ®istry, DEFAULT_DB_URL.to_owned()).await + }); + } + + /// Check if provided error message does match with + /// the [`jsonrpsee::core::ClientError::Call`] Error variant + pub fn rpc_call_error_msg_matches( + result: Result, + raw_msg: &str, + ) -> bool { + let err_obj: ErrorObject = serde_json::from_str(raw_msg).unwrap(); + + result.is_err_and(|err| match err { + jsonrpsee::core::ClientError::Call(owned_obj) => { + owned_obj.message() == ErrorObject::into_owned(err_obj).message() + } + _ => false, + }) + } + + /// Set up a test indexer fetching from a REST endpoint served by the given + /// Simulacrum. + pub async fn start_simulacrum_rest_api_with_write_indexer( + sim: Arc, + ) -> ( + JoinHandle<()>, + PgIndexerStore, + JoinHandle>, + ) { + let server_url: SocketAddr = format!("127.0.0.1:{}", DEFAULT_SERVER_PORT) + .parse() + .unwrap(); + + let server_handle = tokio::spawn(async move { + let chain_id = (*sim + .get_checkpoint_by_sequence_number(0) + .unwrap() + .unwrap() + .digest()) + .into(); + + iota_rest_api::RestService::new_without_version(sim, chain_id) + .start_service(server_url, Some("/rest".to_owned())) + .await; + }); + // Starts indexer + let (pg_store, pg_handle) = start_test_indexer( + Some(DEFAULT_DB_URL.to_owned()), + format!("http://{}", server_url), + ReaderWriterConfig::writer_mode(None), + ) + .await; + (server_handle, pg_store, pg_handle) + } +} diff --git a/crates/iota-indexer/tests/ingestion_tests.rs b/crates/iota-indexer/tests/ingestion_tests.rs index 3e159204088..f04f9a1f13d 100644 --- a/crates/iota-indexer/tests/ingestion_tests.rs +++ b/crates/iota-indexer/tests/ingestion_tests.rs @@ -1,10 +1,11 @@ // Copyright (c) Mysten Labs, Inc. // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 - +#[cfg(feature = "pg_integration")] +mod common; #[cfg(feature = "pg_integration")] mod ingestion_tests { - use std::{net::SocketAddr, sync::Arc, time::Duration}; + use std::sync::Arc; use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; use iota_indexer::{ @@ -12,12 +13,13 @@ mod ingestion_tests { errors::{Context, IndexerError}, models::transactions::StoredTransaction, schema::transactions, - store::{indexer_store::IndexerStore, PgIndexerStore}, - test_utils::{start_test_indexer, ReaderWriterConfig}, }; - use iota_types::{base_types::IotaAddress, effects::TransactionEffectsAPI, storage::ReadStore}; + use iota_types::{base_types::IotaAddress, effects::TransactionEffectsAPI}; use simulacrum::Simulacrum; - use tokio::task::JoinHandle; + + use crate::common::pg_integration::{ + indexer_wait_for_checkpoint, start_simulacrum_rest_api_with_write_indexer, + }; macro_rules! read_only_blocking { ($pool:expr, $query:expr) => {{ @@ -30,66 +32,6 @@ mod ingestion_tests { }}; } - const DEFAULT_SERVER_PORT: u16 = 3000; - const DEFAULT_DB_URL: &str = "postgres://postgres:postgrespw@localhost:5432/iota_indexer"; - - /// Set up a test indexer fetching from a REST endpoint served by the given - /// Simulacrum. - async fn set_up( - sim: Arc, - ) -> ( - JoinHandle<()>, - PgIndexerStore, - JoinHandle>, - ) { - let server_url: SocketAddr = format!("127.0.0.1:{}", DEFAULT_SERVER_PORT) - .parse() - .unwrap(); - - let server_handle = tokio::spawn(async move { - let chain_id = (*sim - .get_checkpoint_by_sequence_number(0) - .unwrap() - .unwrap() - .digest()) - .into(); - - iota_rest_api::RestService::new_without_version(sim, chain_id) - .start_service(server_url, Some("/rest".to_owned())) - .await; - }); - // Starts indexer - let (pg_store, pg_handle) = start_test_indexer( - Some(DEFAULT_DB_URL.to_owned()), - format!("http://{}", server_url), - ReaderWriterConfig::writer_mode(None), - ) - .await; - (server_handle, pg_store, pg_handle) - } - - /// Wait for the indexer to catch up to the given checkpoint sequence - /// number. - async fn wait_for_checkpoint( - pg_store: &PgIndexerStore, - checkpoint_sequence_number: u64, - ) -> Result<(), IndexerError> { - tokio::time::timeout(Duration::from_secs(10), async { - while { - let cp_opt = pg_store - .get_latest_tx_checkpoint_sequence_number() - .await - .unwrap(); - cp_opt.is_none() || (cp_opt.unwrap() < checkpoint_sequence_number) - } { - tokio::time::sleep(Duration::from_secs(1)).await; - } - }) - .await - .expect("Timeout waiting for indexer to catchup to checkpoint"); - Ok(()) - } - #[tokio::test] pub async fn test_transaction_table() -> Result<(), IndexerError> { let mut sim = Simulacrum::new(); @@ -103,10 +45,10 @@ mod ingestion_tests { // Create a checkpoint which should include the transaction we executed. let checkpoint = sim.create_checkpoint(); - let (_, pg_store, _) = set_up(Arc::new(sim)).await; + let (_, pg_store, _) = start_simulacrum_rest_api_with_write_indexer(Arc::new(sim)).await; // Wait for the indexer to catch up to the checkpoint. - wait_for_checkpoint(&pg_store, 1).await?; + indexer_wait_for_checkpoint(&pg_store, 1).await; let digest = effects.transaction_digest(); diff --git a/crates/iota-indexer/tests/rpc-tests/main.rs b/crates/iota-indexer/tests/rpc-tests/main.rs new file mode 100644 index 00000000000..3768cb73acf --- /dev/null +++ b/crates/iota-indexer/tests/rpc-tests/main.rs @@ -0,0 +1,9 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(feature = "pg_integration")] +#[path = "../common/mod.rs"] +mod common; + +#[cfg(feature = "pg_integration")] +mod read_api; diff --git a/crates/iota-indexer/tests/rpc-tests/read_api.rs b/crates/iota-indexer/tests/rpc-tests/read_api.rs new file mode 100644 index 00000000000..b3c1d989814 --- /dev/null +++ b/crates/iota-indexer/tests/rpc-tests/read_api.rs @@ -0,0 +1,1215 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr; + +use iota_config::node::RunWithRange; +use iota_json_rpc_api::{IndexerApiClient, ReadApiClient}; +use iota_json_rpc_types::{ + CheckpointId, IotaGetPastObjectRequest, IotaObjectDataOptions, IotaObjectResponse, + IotaObjectResponseQuery, IotaTransactionBlockResponse, IotaTransactionBlockResponseOptions, +}; +use iota_types::{ + base_types::{ObjectID, SequenceNumber}, + digests::TransactionDigest, + error::IotaObjectResponseError, +}; +use serial_test::serial; + +use crate::common::pg_integration::{ + indexer_wait_for_checkpoint, rpc_call_error_msg_matches, + start_test_cluster_with_read_write_indexer, +}; + +fn is_ascending(vec: &[u64]) -> bool { + vec.windows(2).all(|window| window[0] <= window[1]) +} +fn is_descending(vec: &[u64]) -> bool { + vec.windows(2).all(|window| window[0] >= window[1]) +} + +/// Checks if +/// [`iota_json_rpc_types::IotaTransactionBlockResponse`] match to the provided +/// [`iota_json_rpc_types::IotaTransactionBlockResponseOptions`] filters +fn match_transaction_block_resp_options( + expected_options: &IotaTransactionBlockResponseOptions, + responses: &[IotaTransactionBlockResponse], +) -> bool { + responses + .iter() + .map(|iota_tx_block_resp| IotaTransactionBlockResponseOptions { + show_input: iota_tx_block_resp.transaction.is_some(), + show_raw_input: !iota_tx_block_resp.raw_transaction.is_empty(), + show_effects: iota_tx_block_resp.effects.is_some(), + show_events: iota_tx_block_resp.events.is_some(), + show_object_changes: iota_tx_block_resp.object_changes.is_some(), + show_balance_changes: iota_tx_block_resp.balance_changes.is_some(), + show_raw_effects: !iota_tx_block_resp.raw_effects.is_empty(), + }) + .all(|actual_options| actual_options.eq(expected_options)) +} + +async fn get_object_with_options(options: IotaObjectDataOptions) { + let (cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + let address = cluster.get_address_0(); + + let fullnode_objects = cluster + .rpc_client() + .get_owned_objects( + address, + Some(IotaObjectResponseQuery::new_with_options(options.clone())), + None, + None, + ) + .await + .unwrap(); + + for obj in fullnode_objects.data { + let indexer_obj = indexer_client + .get_object(obj.object_id().unwrap(), Some(options.clone())) + .await + .unwrap(); + + assert_eq!(obj, indexer_obj); + } +} + +async fn multi_get_objects_with_options(options: IotaObjectDataOptions) { + let (cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + let address = cluster.get_address_0(); + + let fullnode_objects = cluster + .rpc_client() + .get_owned_objects( + address, + Some(IotaObjectResponseQuery::new_with_options(options.clone())), + None, + None, + ) + .await + .unwrap(); + + let object_ids = fullnode_objects + .data + .iter() + .map(|iota_object| iota_object.object_id().unwrap()) + .collect::>(); + + let indexer_objects = indexer_client + .multi_get_objects(object_ids, Some(options)) + .await + .unwrap(); + + assert_eq!(fullnode_objects.data, indexer_objects); +} + +async fn get_transaction_block_with_options(options: IotaTransactionBlockResponseOptions) { + let (cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + + let fullnode_checkpoint = cluster + .rpc_client() + .get_checkpoint(CheckpointId::SequenceNumber(0)) + .await + .unwrap(); + + let tx_digest = *fullnode_checkpoint.transactions.first().unwrap(); + + let fullnode_tx = cluster + .rpc_client() + .get_transaction_block(tx_digest, Some(options.clone())) + .await + .unwrap(); + + let tx = indexer_client + .get_transaction_block(tx_digest, Some(options.clone())) + .await + .unwrap(); + + // `IotaTransactionBlockResponse` does have a custom PartialEq impl which does + // not match all options filters but is still good to check if both tx does + // match + assert_eq!(fullnode_tx, tx); + + assert!( + match_transaction_block_resp_options(&options, &[fullnode_tx]), + "fullnode transaction block assertion failed" + ); + assert!( + match_transaction_block_resp_options(&options, &[tx]), + "indexer transaction block assertion failed" + ); +} + +async fn multi_get_transaction_blocks_with_options(options: IotaTransactionBlockResponseOptions) { + let (cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 3).await; + + let fullnode_checkpoints = cluster + .rpc_client() + .get_checkpoints(None, Some(3), false) + .await + .unwrap(); + + let digests = fullnode_checkpoints + .data + .into_iter() + .flat_map(|c| c.transactions) + .collect::>(); + + let fullnode_txs = cluster + .rpc_client() + .multi_get_transaction_blocks(digests.clone(), Some(options.clone())) + .await + .unwrap(); + + let indexer_txs = indexer_client + .multi_get_transaction_blocks(digests, Some(options.clone())) + .await + .unwrap(); + + // `IotaTransactionBlockResponse` does have a custom PartialEq impl which does + // not match all options filters but is still good to check if both tx does + // match + assert_eq!(fullnode_txs, indexer_txs); + + assert!( + match_transaction_block_resp_options(&options, &fullnode_txs), + "fullnode multi transaction blocks assertion failed" + ); + assert!( + match_transaction_block_resp_options(&options, &indexer_txs), + "indexer multi transaction blocks assertion failed" + ); +} + +#[tokio::test] +#[serial] +async fn get_checkpoint_by_seq_num() { + let (cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + + let fullnode_checkpoint = cluster + .rpc_client() + .get_checkpoint(CheckpointId::SequenceNumber(0)) + .await + .unwrap(); + + let indexer_checkpoint = indexer_client + .get_checkpoint(CheckpointId::SequenceNumber(0)) + .await + .unwrap(); + + assert_eq!(fullnode_checkpoint, indexer_checkpoint); +} + +#[tokio::test] +#[serial] +async fn get_checkpoint_by_seq_num_not_found() { + let (_cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + + let result = indexer_client + .get_checkpoint(CheckpointId::SequenceNumber(100000000000)) + .await; + + assert!(rpc_call_error_msg_matches( + result, + r#"{"code":-32603,"message":"Invalid argument with error: `Checkpoint SequenceNumber(100000000000) not found`"}"#, + )); +} + +#[tokio::test] +#[serial] +async fn get_checkpoint_by_digest() { + let (cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + + let fullnode_checkpoint = cluster + .rpc_client() + .get_checkpoint(CheckpointId::SequenceNumber(0)) + .await + .unwrap(); + + let indexer_checkpoint = indexer_client + .get_checkpoint(CheckpointId::Digest(fullnode_checkpoint.digest)) + .await + .unwrap(); + + assert_eq!(fullnode_checkpoint, indexer_checkpoint); +} + +#[tokio::test] +#[serial] +async fn get_checkpoint_by_digest_not_found() { + let (_cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + + let result = indexer_client + .get_checkpoint(CheckpointId::Digest([0; 32].into())) + .await; + + assert!(rpc_call_error_msg_matches( + result, + r#"{"code":-32603,"message":"Invalid argument with error: `Checkpoint Digest(CheckpointDigest(11111111111111111111111111111111)) not found`"}"#, + )); +} + +#[tokio::test] +#[serial] +async fn get_checkpoints_all_ascending() { + let (_cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 3).await; + + let indexer_checkpoint = indexer_client + .get_checkpoints(None, None, false) + .await + .unwrap(); + + let seq_numbers = indexer_checkpoint + .data + .iter() + .map(|c| c.sequence_number) + .collect::>(); + + assert!(is_ascending(&seq_numbers)); +} + +#[tokio::test] +#[serial] +async fn get_checkpoints_all_descending() { + let (_cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 3).await; + + let indexer_checkpoint = indexer_client + .get_checkpoints(None, None, true) + .await + .unwrap(); + + let seq_numbers = indexer_checkpoint + .data + .iter() + .map(|c| c.sequence_number) + .collect::>(); + + assert!(is_descending(&seq_numbers)); +} + +#[tokio::test] +#[serial] +async fn get_checkpoints_by_cursor_and_limit_one_descending() { + let (_cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 3).await; + + let indexer_checkpoint = indexer_client + .get_checkpoints(Some(1.into()), Some(1), true) + .await + .unwrap(); + + assert_eq!( + vec![0], + indexer_checkpoint + .data + .into_iter() + .map(|c| c.sequence_number) + .collect::>() + ); +} + +#[tokio::test] +#[serial] +async fn get_checkpoints_by_cursor_and_limit_one_ascending() { + let (_cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 3).await; + + let indexer_checkpoint = indexer_client + .get_checkpoints(Some(1.into()), Some(1), false) + .await + .unwrap(); + + assert_eq!( + vec![2], + indexer_checkpoint + .data + .into_iter() + .map(|c| c.sequence_number) + .collect::>() + ); +} + +#[tokio::test] +#[serial] +async fn get_checkpoints_by_cursor_zero_and_limit_ascending() { + let (_cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 3).await; + + let indexer_checkpoint = indexer_client + .get_checkpoints(Some(0.into()), Some(3), false) + .await + .unwrap(); + + assert_eq!( + vec![1, 2, 3], + indexer_checkpoint + .data + .into_iter() + .map(|c| c.sequence_number) + .collect::>() + ); +} + +#[tokio::test] +#[serial] +async fn get_checkpoints_by_cursor_zero_and_limit_descending() { + let (_cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 3).await; + + let indexer_checkpoint = indexer_client + .get_checkpoints(Some(0.into()), Some(3), true) + .await + .unwrap(); + + assert_eq!( + Vec::::default(), + indexer_checkpoint + .data + .into_iter() + .map(|c| c.sequence_number) + .collect::>() + ); +} + +#[tokio::test] +#[serial] +async fn get_checkpoints_by_cursor_and_limit_ascending() { + let (_cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 6).await; + + let indexer_checkpoint = indexer_client + .get_checkpoints(Some(3.into()), Some(3), false) + .await + .unwrap(); + + assert_eq!( + vec![4, 5, 6], + indexer_checkpoint + .data + .into_iter() + .map(|c| c.sequence_number) + .collect::>() + ); +} + +#[tokio::test] +#[serial] +async fn get_checkpoints_by_cursor_and_limit_descending() { + let (_cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 3).await; + + let indexer_checkpoint = indexer_client + .get_checkpoints(Some(3.into()), Some(3), true) + .await + .unwrap(); + + assert_eq!( + vec![2, 1, 0], + indexer_checkpoint + .data + .into_iter() + .map(|c| c.sequence_number) + .collect::>() + ); +} + +#[tokio::test] +#[serial] +async fn get_checkpoints_invalid_limit() { + let (_cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 3).await; + + let result = indexer_client.get_checkpoints(None, Some(0), false).await; + + assert!(rpc_call_error_msg_matches( + result, + r#"{"code":-32602,"message":"Page size limit cannot be smaller than 1"}"#, + )); +} + +#[tokio::test] +#[serial] +async fn get_object() { + let (cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + let address = cluster.get_address_0(); + + let fullnode_objects = cluster + .rpc_client() + .get_owned_objects(address, None, None, None) + .await + .unwrap(); + + for obj in fullnode_objects.data { + let indexer_obj = indexer_client + .get_object(obj.object_id().unwrap(), None) + .await + .unwrap(); + assert_eq!(obj, indexer_obj) + } +} + +#[tokio::test] +#[serial] +async fn get_object_not_found() { + let (_cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + + let indexer_obj = indexer_client + .get_object( + ObjectID::from_str( + "0x9a934a2644c4ca2decbe3d126d80720429c5e31896aa756765afa23ae2cb4b99", + ) + .unwrap(), + None, + ) + .await + .unwrap(); + + assert_eq!( + indexer_obj, + IotaObjectResponse { + data: None, + error: Some(IotaObjectResponseError::NotExists { + object_id: "0x9a934a2644c4ca2decbe3d126d80720429c5e31896aa756765afa23ae2cb4b99" + .parse() + .unwrap() + }) + } + ) +} + +#[tokio::test] +#[serial] +async fn get_object_with_bcs_lossless() { + get_object_with_options(IotaObjectDataOptions::bcs_lossless()).await; +} + +#[tokio::test] +#[serial] +async fn get_object_with_full_content() { + get_object_with_options(IotaObjectDataOptions::full_content()).await; +} + +#[tokio::test] +#[serial] +async fn get_object_with_bcs() { + get_object_with_options(IotaObjectDataOptions::default().with_bcs()).await; +} + +#[tokio::test] +#[serial] +async fn get_object_with_content() { + get_object_with_options(IotaObjectDataOptions::default().with_content()).await; +} + +#[tokio::test] +#[serial] +async fn get_object_with_display() { + get_object_with_options(IotaObjectDataOptions::default().with_display()).await; +} + +#[tokio::test] +#[serial] +async fn get_object_with_owner() { + get_object_with_options(IotaObjectDataOptions::default().with_owner()).await; +} + +#[tokio::test] +#[serial] +async fn get_object_with_previous_transaction() { + get_object_with_options(IotaObjectDataOptions::default().with_previous_transaction()).await; +} + +#[tokio::test] +#[serial] +async fn get_object_with_type() { + get_object_with_options(IotaObjectDataOptions::default().with_type()).await; +} + +#[tokio::test] +#[serial] +async fn get_object_with_storage_rebate() { + get_object_with_options(IotaObjectDataOptions { + show_storage_rebate: true, + ..Default::default() + }) + .await; +} + +#[tokio::test] +#[serial] +async fn multi_get_objects() { + let (cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + let address = cluster.get_address_0(); + + let fullnode_objects = cluster + .rpc_client() + .get_owned_objects(address, None, None, None) + .await + .unwrap(); + + let object_ids = fullnode_objects + .data + .iter() + .map(|iota_object| iota_object.object_id().unwrap()) + .collect(); + + let indexer_objects = indexer_client + .multi_get_objects(object_ids, None) + .await + .unwrap(); + + assert_eq!(fullnode_objects.data, indexer_objects); +} + +#[tokio::test] +#[serial] +async fn multi_get_objects_not_found() { + let (_cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + + let object_ids = vec![ + ObjectID::from_str("0x9a934a2644c4ca2decbe3d126d80720429c5e31896aa756765afa23ae2cb4b99") + .unwrap(), + ObjectID::from_str("0x1a934a7644c4cf2decbe3d126d80720429c5e30896aa756765afa23af3cb4b82") + .unwrap(), + ]; + + let indexer_objects = indexer_client + .multi_get_objects(object_ids, None) + .await + .unwrap(); + + assert_eq!( + indexer_objects, + vec![ + IotaObjectResponse { + data: None, + error: Some(IotaObjectResponseError::NotExists { + object_id: "0x9a934a2644c4ca2decbe3d126d80720429c5e31896aa756765afa23ae2cb4b99" + .parse() + .unwrap() + }) + }, + IotaObjectResponse { + data: None, + error: Some(IotaObjectResponseError::NotExists { + object_id: "0x1a934a7644c4cf2decbe3d126d80720429c5e30896aa756765afa23af3cb4b82" + .parse() + .unwrap() + }) + } + ] + ) +} + +#[tokio::test] +#[serial] +async fn multi_get_objects_found_and_not_found() { + let (cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + let address = cluster.get_address_0(); + + let fullnode_objects = cluster + .rpc_client() + .get_owned_objects(address, None, None, None) + .await + .unwrap(); + + let mut object_ids = fullnode_objects + .data + .iter() + .map(|iota_object| iota_object.object_id().unwrap()) + .collect::>(); + + object_ids.extend_from_slice(&[ + ObjectID::from_str("0x9a934a2644c4ca2decbe3d126d80720429c5e31896aa756765afa23ae2cb4b99") + .unwrap(), + ObjectID::from_str("0x1a934a7644c4cf2decbe3d126d80720429c5e30896aa756765afa23af3cb4b82") + .unwrap(), + ]); + + let indexer_objects = indexer_client + .multi_get_objects(object_ids, None) + .await + .unwrap(); + + let obj_found_num = indexer_objects + .iter() + .filter(|obj_response| obj_response.data.is_some()) + .count(); + + assert_eq!(5, obj_found_num); + + let obj_not_found_num = indexer_objects + .iter() + .filter(|obj_response| obj_response.error.is_some()) + .count(); + + assert_eq!(2, obj_not_found_num); +} + +#[tokio::test] +#[serial] +async fn multi_get_objects_with_bcs_lossless() { + multi_get_objects_with_options(IotaObjectDataOptions::bcs_lossless()).await; +} + +#[tokio::test] +#[serial] +async fn multi_get_objects_with_full_content() { + multi_get_objects_with_options(IotaObjectDataOptions::full_content()).await; +} + +#[tokio::test] +#[serial] +async fn multi_get_objects_with_bcs() { + multi_get_objects_with_options(IotaObjectDataOptions::default().with_bcs()).await; +} + +#[tokio::test] +#[serial] +async fn multi_get_objects_with_content() { + multi_get_objects_with_options(IotaObjectDataOptions::default().with_content()).await; +} + +#[tokio::test] +#[serial] +async fn multi_get_objects_with_display() { + multi_get_objects_with_options(IotaObjectDataOptions::default().with_display()).await; +} + +#[tokio::test] +#[serial] +async fn multi_get_objects_with_owner() { + multi_get_objects_with_options(IotaObjectDataOptions::default().with_owner()).await; +} + +#[tokio::test] +#[serial] +async fn multi_get_objects_with_previous_transaction() { + multi_get_objects_with_options(IotaObjectDataOptions::default().with_previous_transaction()) + .await; +} + +#[tokio::test] +#[serial] +async fn multi_get_objects_with_type() { + multi_get_objects_with_options(IotaObjectDataOptions::default().with_type()).await; +} + +#[tokio::test] +#[serial] +async fn multi_get_objects_with_storage_rebate() { + multi_get_objects_with_options(IotaObjectDataOptions { + show_storage_rebate: true, + ..Default::default() + }) + .await; +} + +#[tokio::test] +#[serial] +async fn get_events() { + let (cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + + let fullnode_checkpoint = cluster + .rpc_client() + .get_checkpoint(CheckpointId::SequenceNumber(0)) + .await + .unwrap(); + + let events = indexer_client + .get_events(*fullnode_checkpoint.transactions.first().unwrap()) + .await + .unwrap(); + + assert!(!events.is_empty()); +} + +#[tokio::test] +#[serial] +async fn get_events_not_found() { + let (_cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + + let result = indexer_client.get_events(TransactionDigest::ZERO).await; + + assert!(rpc_call_error_msg_matches( + result, + r#"{"code":-32603,"message":"Indexer failed to read PostgresDB with error: `Record not found`"}"#, + )) +} + +#[tokio::test] +#[serial] +async fn get_transaction_block() { + let (cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + + let fullnode_checkpoint = cluster + .rpc_client() + .get_checkpoint(CheckpointId::SequenceNumber(0)) + .await + .unwrap(); + + let tx_digest = *fullnode_checkpoint.transactions.first().unwrap(); + + let tx = indexer_client + .get_transaction_block(tx_digest, None) + .await + .unwrap(); + + assert_eq!(tx_digest, tx.digest); +} + +#[tokio::test] +#[serial] +async fn get_transaction_block_not_found() { + let (_cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + + let result = indexer_client + .get_transaction_block(TransactionDigest::ZERO, None) + .await; + + assert!(rpc_call_error_msg_matches( + result, + r#"{"code":-32603,"message":"Invalid argument with error: `Transaction 11111111111111111111111111111111 not found`"}"#, + )); +} + +#[tokio::test] +#[serial] +async fn get_transaction_block_with_full_content() { + get_transaction_block_with_options(IotaTransactionBlockResponseOptions::full_content()).await; +} + +#[tokio::test] +#[serial] +async fn get_transaction_block_with_full_content_and_with_raw_effects() { + get_transaction_block_with_options( + IotaTransactionBlockResponseOptions::full_content().with_raw_effects(), + ) + .await; +} + +#[tokio::test] +#[serial] +async fn get_transaction_block_with_raw_input() { + get_transaction_block_with_options( + IotaTransactionBlockResponseOptions::default().with_raw_input(), + ) + .await; +} + +#[tokio::test] +#[serial] +async fn get_transaction_block_with_effects() { + get_transaction_block_with_options( + IotaTransactionBlockResponseOptions::default().with_effects(), + ) + .await; +} + +#[tokio::test] +#[serial] +async fn get_transaction_block_with_events() { + get_transaction_block_with_options( + IotaTransactionBlockResponseOptions::default().with_events(), + ) + .await; +} + +#[tokio::test] +#[serial] +async fn get_transaction_block_with_balance_changes() { + get_transaction_block_with_options( + IotaTransactionBlockResponseOptions::default().with_balance_changes(), + ) + .await; +} + +#[tokio::test] +#[serial] +async fn get_transaction_block_with_object_changes() { + get_transaction_block_with_options( + IotaTransactionBlockResponseOptions::default().with_object_changes(), + ) + .await; +} + +#[tokio::test] +#[serial] +async fn get_transaction_block_with_raw_effects() { + get_transaction_block_with_options( + IotaTransactionBlockResponseOptions::default().with_raw_effects(), + ) + .await; +} + +#[tokio::test] +#[serial] +async fn get_transaction_block_with_input() { + get_transaction_block_with_options(IotaTransactionBlockResponseOptions::default().with_input()) + .await; +} + +#[tokio::test] +#[serial] +async fn multi_get_transaction_blocks() { + let (cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 3).await; + + let fullnode_checkpoints = cluster + .rpc_client() + .get_checkpoints(None, Some(3), false) + .await + .unwrap(); + + let digests = fullnode_checkpoints + .data + .into_iter() + .flat_map(|c| c.transactions) + .collect::>(); + + let fullnode_txs = cluster + .rpc_client() + .multi_get_transaction_blocks(digests.clone(), None) + .await + .unwrap(); + + let indexer_txs = indexer_client + .multi_get_transaction_blocks(digests, None) + .await + .unwrap(); + + assert_eq!(fullnode_txs, indexer_txs); +} + +#[tokio::test] +#[serial] +async fn multi_get_transaction_blocks_with_full_content() { + multi_get_transaction_blocks_with_options(IotaTransactionBlockResponseOptions::full_content()) + .await; +} + +#[tokio::test] +#[serial] +async fn multi_get_transaction_blocks_with_full_content_and_with_raw_effects() { + multi_get_transaction_blocks_with_options( + IotaTransactionBlockResponseOptions::full_content().with_raw_effects(), + ) + .await; +} + +#[tokio::test] +#[serial] +async fn multi_get_transaction_blocks_with_raw_input() { + multi_get_transaction_blocks_with_options( + IotaTransactionBlockResponseOptions::default().with_raw_input(), + ) + .await; +} + +#[tokio::test] +#[serial] +async fn multi_get_transaction_blocks_with_effects() { + multi_get_transaction_blocks_with_options( + IotaTransactionBlockResponseOptions::default().with_effects(), + ) + .await; +} + +#[tokio::test] +#[serial] +async fn multi_get_transaction_blocks_with_events() { + multi_get_transaction_blocks_with_options( + IotaTransactionBlockResponseOptions::default().with_events(), + ) + .await; +} + +#[tokio::test] +#[serial] +async fn multi_get_transaction_blocks_with_balance_changes() { + multi_get_transaction_blocks_with_options( + IotaTransactionBlockResponseOptions::default().with_balance_changes(), + ) + .await; +} + +#[tokio::test] +#[serial] +async fn multi_get_transaction_blocks_with_object_changes() { + multi_get_transaction_blocks_with_options( + IotaTransactionBlockResponseOptions::default().with_object_changes(), + ) + .await; +} + +#[tokio::test] +#[serial] +async fn multi_get_transaction_blocks_with_raw_effects() { + multi_get_transaction_blocks_with_options( + IotaTransactionBlockResponseOptions::default().with_raw_effects(), + ) + .await; +} + +#[tokio::test] +#[serial] +async fn multi_get_transaction_blocks_with_input() { + multi_get_transaction_blocks_with_options( + IotaTransactionBlockResponseOptions::default().with_input(), + ) + .await; +} + +#[tokio::test] +#[serial] +async fn get_protocol_config() { + let (cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + + let fullnode_protocol_config = cluster + .rpc_client() + .get_protocol_config(None) + .await + .unwrap(); + + let indexer_protocol_config = indexer_client.get_protocol_config(None).await.unwrap(); + + assert_eq!(fullnode_protocol_config, indexer_protocol_config); + + let indexer_protocol_config = indexer_client + .get_protocol_config(Some(1u64.into())) + .await + .unwrap(); + + assert_eq!(fullnode_protocol_config, indexer_protocol_config); +} + +#[tokio::test] +#[serial] +async fn get_protocol_config_invalid_protocol_version() { + let (_cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + + let result = indexer_client + .get_protocol_config(Some(100u64.into())) + .await; + + assert!(rpc_call_error_msg_matches( + result, + r#"{"code":-32603,"message":"Unsupported protocol version requested. Min supported: 1, max supported: 1"}"#, + )); +} + +#[tokio::test] +#[serial] +async fn get_chain_identifier() { + let (cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + + let fullnode_chain_identifier = cluster.rpc_client().get_chain_identifier().await.unwrap(); + + let indexer_chain_identifier = indexer_client.get_chain_identifier().await.unwrap(); + + assert_eq!(fullnode_chain_identifier, indexer_chain_identifier) +} + +#[tokio::test] +#[serial] +async fn get_total_transaction_blocks() { + let stop_after_checkpoint_seq = 5; + let (cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(Some(stop_after_checkpoint_seq)).await; + + let run_with_range = cluster + .wait_for_run_with_range_shutdown_signal() + .await + .unwrap(); + + assert!(matches!( + run_with_range, + RunWithRange::Checkpoint(checkpoint_seq_num) if checkpoint_seq_num == stop_after_checkpoint_seq + )); + + // ensure the highest synced checkpoint matches + assert!(cluster.fullnode_handle.iota_node.with(|node| { + node.state() + .get_checkpoint_store() + .get_highest_executed_checkpoint_seq_number() + .unwrap() + == Some(stop_after_checkpoint_seq) + })); + + let checkpoint = cluster + .fullnode_handle + .iota_node + .with(|node| { + node.state() + .get_checkpoint_store() + .get_checkpoint_by_sequence_number(stop_after_checkpoint_seq) + .unwrap() + }) + .unwrap(); + + indexer_wait_for_checkpoint(&pg_store, stop_after_checkpoint_seq).await; + + let total_transaction_blocks = indexer_client + .get_total_transaction_blocks() + .await + .unwrap() + .into_inner(); + + assert_eq!( + checkpoint.network_total_transactions, + total_transaction_blocks + ); +} + +#[tokio::test] +#[serial] +async fn get_latest_checkpoint_sequence_number() { + let stop_after_checkpoint_seq = 5; + let (cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(Some(stop_after_checkpoint_seq)).await; + + let run_with_range = cluster + .wait_for_run_with_range_shutdown_signal() + .await + .unwrap(); + + assert!(matches!( + run_with_range, + RunWithRange::Checkpoint(checkpoint_seq_num) if checkpoint_seq_num == stop_after_checkpoint_seq + )); + + // ensure the highest synced checkpoint matches + assert!(cluster.fullnode_handle.iota_node.with(|node| { + node.state() + .get_checkpoint_store() + .get_highest_executed_checkpoint_seq_number() + .unwrap() + == Some(stop_after_checkpoint_seq) + })); + + let fullnode_latest_checkpoint_seq_number = cluster + .rpc_client() + .get_latest_checkpoint_sequence_number() + .await + .unwrap() + .into_inner(); + + indexer_wait_for_checkpoint(&pg_store, stop_after_checkpoint_seq).await; + + let latest_checkpoint_seq_number = indexer_client + .get_latest_checkpoint_sequence_number() + .await + .unwrap() + .into_inner(); + + assert!( + (stop_after_checkpoint_seq == latest_checkpoint_seq_number) + && (stop_after_checkpoint_seq == fullnode_latest_checkpoint_seq_number) + ); +} + +#[tokio::test] +#[serial] +async fn try_get_past_object() { + let (_cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + + let result = indexer_client + .try_get_past_object(ObjectID::random(), SequenceNumber::new(), None) + .await; + assert!(rpc_call_error_msg_matches( + result, + r#"{"code":-32601,"message":"Method not found"}"# + )); +} + +#[tokio::test] +#[serial] +async fn try_multi_get_past_objects() { + let (_cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + + let result = indexer_client + .try_multi_get_past_objects( + vec![IotaGetPastObjectRequest { + object_id: ObjectID::random(), + version: SequenceNumber::new(), + }], + None, + ) + .await; + assert!(rpc_call_error_msg_matches( + result, + r#"{"code":-32601,"message":"Method not found"}"# + )); +} + +#[tokio::test] +#[serial] +async fn get_loaded_child_objects() { + let (_cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + + let result = indexer_client + .get_loaded_child_objects(TransactionDigest::ZERO) + .await; + assert!(rpc_call_error_msg_matches( + result, + r#"{"code":-32601,"message":"Method not found"}"# + )); +} diff --git a/crates/iota-json-rpc/src/read_api.rs b/crates/iota-json-rpc/src/read_api.rs index 26d26801e9d..6818e951ede 100644 --- a/crates/iota-json-rpc/src/read_api.rs +++ b/crates/iota-json-rpc/src/read_api.rs @@ -1348,6 +1348,17 @@ fn convert_to_response( response.transaction = Some(tx_block); } + if opts.show_raw_effects { + let raw_effects = cache + .effects + .as_ref() + .map(bcs::to_bytes) + .transpose() + .map_err(|e| anyhow!("Failed to serialize raw effects with error: {e}"))? + .unwrap_or_default(); + response.raw_effects = raw_effects; + } + if opts.show_effects && cache.effects.is_some() { let effects = cache.effects.unwrap().try_into().map_err(|e| { anyhow!( @@ -1372,6 +1383,7 @@ fn convert_to_response( if opts.show_object_changes { response.object_changes = cache.object_changes; } + Ok(response) } From 1ac3eb61040352cf8804067ae07013ba381c2f61 Mon Sep 17 00:00:00 2001 From: tomxey Date: Mon, 30 Sep 2024 09:36:21 +0200 Subject: [PATCH 02/24] feat(iota-indexer): Extended api tests for iota-indexer (#2619) * feat(iota-indexer): Extended api tests for iota-indexer * Rustfmt, removed unused imports, small cleanups * Remove underscore prefixes from function names * Add reason for tests to be ignored * Simplify `add_test_epochs_to_simulacrum` * Remove test_ prefixes from the tests * Update ignore reasons for metrics tests * Do not use pattern matching in asserts * Make test data preparation more verbose --- crates/iota-indexer/tests/common/mod.rs | 35 +- .../tests/rpc-tests/extended_api.rs | 509 ++++++++++++++++++ crates/iota-indexer/tests/rpc-tests/main.rs | 2 + 3 files changed, 543 insertions(+), 3 deletions(-) create mode 100644 crates/iota-indexer/tests/rpc-tests/extended_api.rs diff --git a/crates/iota-indexer/tests/common/mod.rs b/crates/iota-indexer/tests/common/mod.rs index 158c434902f..493642162ea 100644 --- a/crates/iota-indexer/tests/common/mod.rs +++ b/crates/iota-indexer/tests/common/mod.rs @@ -124,6 +124,12 @@ pub mod pg_integration { }) } + pub fn get_default_fullnode_rpc_api_addr() -> SocketAddr { + format!("127.0.0.1:{}", DEFAULT_SERVER_PORT) + .parse() + .unwrap() + } + /// Set up a test indexer fetching from a REST endpoint served by the given /// Simulacrum. pub async fn start_simulacrum_rest_api_with_write_indexer( @@ -133,9 +139,7 @@ pub mod pg_integration { PgIndexerStore, JoinHandle>, ) { - let server_url: SocketAddr = format!("127.0.0.1:{}", DEFAULT_SERVER_PORT) - .parse() - .unwrap(); + let server_url = get_default_fullnode_rpc_api_addr(); let server_handle = tokio::spawn(async move { let chain_id = (*sim @@ -158,4 +162,29 @@ pub mod pg_integration { .await; (server_handle, pg_store, pg_handle) } + + pub async fn start_simulacrum_rest_api_with_read_write_indexer( + sim: Arc, + ) -> ( + JoinHandle<()>, + PgIndexerStore, + JoinHandle>, + HttpClient, + ) { + let server_url = get_default_fullnode_rpc_api_addr(); + let (server_handle, pg_store, pg_handle) = + start_simulacrum_rest_api_with_write_indexer(sim).await; + + // start indexer in read mode + start_indexer_reader(format!("http://{}", server_url)); + + // create an RPC client by using the indexer url + let rpc_client = HttpClientBuilder::default() + .build(format!( + "http://{DEFAULT_INDEXER_IP}:{DEFAULT_INDEXER_PORT}" + )) + .unwrap(); + + (server_handle, pg_store, pg_handle, rpc_client) + } } diff --git a/crates/iota-indexer/tests/rpc-tests/extended_api.rs b/crates/iota-indexer/tests/rpc-tests/extended_api.rs new file mode 100644 index 00000000000..85ae15a12c0 --- /dev/null +++ b/crates/iota-indexer/tests/rpc-tests/extended_api.rs @@ -0,0 +1,509 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::{str::FromStr, sync::Arc}; + +use iota_json::{call_args, type_args}; +use iota_json_rpc_api::{ + ExtendedApiClient, IndexerApiClient, ReadApiClient, TransactionBuilderClient, WriteApiClient, +}; +use iota_json_rpc_types::{ + IotaObjectDataOptions, IotaObjectResponseQuery, IotaTransactionBlockResponseOptions, + TransactionBlockBytes, +}; +use iota_types::{ + base_types::{IotaAddress, ObjectID}, + gas_coin::GAS, + quorum_driver_types::ExecuteTransactionRequestType, + storage::ReadStore, + IOTA_FRAMEWORK_ADDRESS, +}; +use serial_test::serial; +use simulacrum::Simulacrum; +use test_cluster::TestCluster; + +use crate::common::pg_integration::{ + indexer_wait_for_checkpoint, start_simulacrum_rest_api_with_read_write_indexer, + start_test_cluster_with_read_write_indexer, +}; + +#[tokio::test] +#[serial] +async fn get_epochs() { + let mut sim = Simulacrum::new(); + + execute_simulacrum_transactions(&mut sim, 15); + add_checkpoints(&mut sim, 300); + sim.advance_epoch(false); + + execute_simulacrum_transactions(&mut sim, 10); + add_checkpoints(&mut sim, 300); + sim.advance_epoch(false); + + execute_simulacrum_transactions(&mut sim, 5); + add_checkpoints(&mut sim, 300); + + let last_checkpoint = sim.get_latest_checkpoint().unwrap(); + + let (_, pg_store, _, indexer_client) = + start_simulacrum_rest_api_with_read_write_indexer(Arc::new(sim)).await; + indexer_wait_for_checkpoint(&pg_store, last_checkpoint.sequence_number).await; + + let epochs = indexer_client.get_epochs(None, None, None).await.unwrap(); + + assert_eq!(epochs.data.len(), 3); + assert_eq!(epochs.has_next_page, false); + + let end_of_epoch_info = epochs.data[0].end_of_epoch_info.as_ref().unwrap(); + assert_eq!(epochs.data[0].epoch, 0); + assert_eq!(epochs.data[0].first_checkpoint_id, 0); + assert_eq!(epochs.data[0].epoch_total_transactions, 17); + assert_eq!(end_of_epoch_info.last_checkpoint_id, 301); + + let end_of_epoch_info = epochs.data[1].end_of_epoch_info.as_ref().unwrap(); + assert_eq!(epochs.data[1].epoch, 1); + assert_eq!(epochs.data[1].first_checkpoint_id, 302); + assert_eq!(epochs.data[1].epoch_total_transactions, 11); + assert_eq!(end_of_epoch_info.last_checkpoint_id, 602); + + assert_eq!(epochs.data[2].epoch, 2); + assert_eq!(epochs.data[2].first_checkpoint_id, 603); + assert_eq!(epochs.data[2].epoch_total_transactions, 0); + assert!(epochs.data[2].end_of_epoch_info.is_none()); +} + +#[tokio::test] +#[serial] +async fn get_epochs_descending() { + let mut sim = Simulacrum::new(); + + execute_simulacrum_transactions(&mut sim, 15); + add_checkpoints(&mut sim, 300); + sim.advance_epoch(false); + + execute_simulacrum_transactions(&mut sim, 10); + add_checkpoints(&mut sim, 300); + sim.advance_epoch(false); + + execute_simulacrum_transactions(&mut sim, 5); + add_checkpoints(&mut sim, 300); + + let last_checkpoint = sim.get_latest_checkpoint().unwrap(); + + let (_, pg_store, _, indexer_client) = + start_simulacrum_rest_api_with_read_write_indexer(Arc::new(sim)).await; + indexer_wait_for_checkpoint(&pg_store, last_checkpoint.sequence_number).await; + + let epochs = indexer_client + .get_epochs(None, None, Some(true)) + .await + .unwrap(); + + let actual_epochs_order = epochs + .data + .iter() + .map(|epoch| epoch.epoch) + .collect::>(); + + assert_eq!(epochs.data.len(), 3); + assert_eq!(epochs.has_next_page, false); + assert_eq!(actual_epochs_order, [2, 1, 0]) +} + +#[tokio::test] +#[serial] +async fn get_epochs_paging() { + let mut sim = Simulacrum::new(); + + execute_simulacrum_transactions(&mut sim, 15); + add_checkpoints(&mut sim, 300); + sim.advance_epoch(false); + + execute_simulacrum_transactions(&mut sim, 10); + add_checkpoints(&mut sim, 300); + sim.advance_epoch(false); + + execute_simulacrum_transactions(&mut sim, 5); + add_checkpoints(&mut sim, 300); + + let last_checkpoint = sim.get_latest_checkpoint().unwrap(); + + let (_, pg_store, _, indexer_client) = + start_simulacrum_rest_api_with_read_write_indexer(Arc::new(sim)).await; + indexer_wait_for_checkpoint(&pg_store, last_checkpoint.sequence_number).await; + + let epochs = indexer_client + .get_epochs(None, Some(2), None) + .await + .unwrap(); + let actual_epochs_order = epochs + .data + .iter() + .map(|epoch| epoch.epoch) + .collect::>(); + + assert_eq!(epochs.data.len(), 2); + assert_eq!(epochs.has_next_page, true); + assert_eq!(epochs.next_cursor, Some(1.into())); + assert_eq!(actual_epochs_order, [0, 1]); + + let epochs = indexer_client + .get_epochs(Some(1.into()), Some(2), None) + .await + .unwrap(); + let actual_epochs_order = epochs + .data + .iter() + .map(|epoch| epoch.epoch) + .collect::>(); + + assert_eq!(epochs.data.len(), 1); + assert_eq!(epochs.has_next_page, false); + assert_eq!(epochs.next_cursor, Some(2.into())); + assert_eq!(actual_epochs_order, [2]); +} + +#[tokio::test] +#[serial] +async fn get_epoch_metrics() { + let mut sim = Simulacrum::new(); + + execute_simulacrum_transactions(&mut sim, 15); + add_checkpoints(&mut sim, 300); + sim.advance_epoch(false); + + execute_simulacrum_transactions(&mut sim, 10); + add_checkpoints(&mut sim, 300); + sim.advance_epoch(false); + + execute_simulacrum_transactions(&mut sim, 5); + add_checkpoints(&mut sim, 300); + + let last_checkpoint = sim.get_latest_checkpoint().unwrap(); + + let (_, pg_store, _, indexer_client) = + start_simulacrum_rest_api_with_read_write_indexer(Arc::new(sim)).await; + indexer_wait_for_checkpoint(&pg_store, last_checkpoint.sequence_number).await; + + let epoch_metrics = indexer_client + .get_epoch_metrics(None, None, None) + .await + .unwrap(); + + assert_eq!(epoch_metrics.data.len(), 3); + assert_eq!(epoch_metrics.has_next_page, false); + + let end_of_epoch_info = epoch_metrics.data[0].end_of_epoch_info.as_ref().unwrap(); + assert_eq!(epoch_metrics.data[0].epoch, 0); + assert_eq!(epoch_metrics.data[0].first_checkpoint_id, 0); + assert_eq!(epoch_metrics.data[0].epoch_total_transactions, 17); + assert_eq!(end_of_epoch_info.last_checkpoint_id, 301); + + let end_of_epoch_info = epoch_metrics.data[1].end_of_epoch_info.as_ref().unwrap(); + assert_eq!(epoch_metrics.data[1].epoch, 1); + assert_eq!(epoch_metrics.data[1].first_checkpoint_id, 302); + assert_eq!(epoch_metrics.data[1].epoch_total_transactions, 11); + assert_eq!(end_of_epoch_info.last_checkpoint_id, 602); + + assert_eq!(epoch_metrics.data[2].epoch, 2); + assert_eq!(epoch_metrics.data[2].first_checkpoint_id, 603); + assert_eq!(epoch_metrics.data[2].epoch_total_transactions, 0); + assert!(epoch_metrics.data[2].end_of_epoch_info.is_none()); +} + +#[tokio::test] +#[serial] +async fn get_epoch_metrics_descending() { + let mut sim = Simulacrum::new(); + + execute_simulacrum_transactions(&mut sim, 15); + add_checkpoints(&mut sim, 300); + sim.advance_epoch(false); + + execute_simulacrum_transactions(&mut sim, 10); + add_checkpoints(&mut sim, 300); + sim.advance_epoch(false); + + execute_simulacrum_transactions(&mut sim, 5); + add_checkpoints(&mut sim, 300); + + let last_checkpoint = sim.get_latest_checkpoint().unwrap(); + + let (_, pg_store, _, indexer_client) = + start_simulacrum_rest_api_with_read_write_indexer(Arc::new(sim)).await; + indexer_wait_for_checkpoint(&pg_store, last_checkpoint.sequence_number).await; + + let epochs = indexer_client + .get_epoch_metrics(None, None, Some(true)) + .await + .unwrap(); + + let actual_epochs_order = epochs + .data + .iter() + .map(|epoch| epoch.epoch) + .collect::>(); + + assert_eq!(epochs.data.len(), 3); + assert_eq!(epochs.has_next_page, false); + assert_eq!(actual_epochs_order, [2, 1, 0]) +} + +#[tokio::test] +#[serial] +async fn get_epoch_metrics_paging() { + let mut sim = Simulacrum::new(); + + execute_simulacrum_transactions(&mut sim, 15); + add_checkpoints(&mut sim, 300); + sim.advance_epoch(false); + + execute_simulacrum_transactions(&mut sim, 10); + add_checkpoints(&mut sim, 300); + sim.advance_epoch(false); + + execute_simulacrum_transactions(&mut sim, 5); + add_checkpoints(&mut sim, 300); + + let last_checkpoint = sim.get_latest_checkpoint().unwrap(); + + let (_, pg_store, _, indexer_client) = + start_simulacrum_rest_api_with_read_write_indexer(Arc::new(sim)).await; + indexer_wait_for_checkpoint(&pg_store, last_checkpoint.sequence_number).await; + + let epochs = indexer_client + .get_epoch_metrics(None, Some(2), None) + .await + .unwrap(); + let actual_epochs_order = epochs + .data + .iter() + .map(|epoch| epoch.epoch) + .collect::>(); + + assert_eq!(epochs.data.len(), 2); + assert_eq!(epochs.has_next_page, true); + assert_eq!(epochs.next_cursor, Some(1.into())); + assert_eq!(actual_epochs_order, [0, 1]); + + let epochs = indexer_client + .get_epoch_metrics(Some(1.into()), Some(2), None) + .await + .unwrap(); + let actual_epochs_order = epochs + .data + .iter() + .map(|epoch| epoch.epoch) + .collect::>(); + + assert_eq!(epochs.data.len(), 1); + assert_eq!(epochs.has_next_page, false); + assert_eq!(epochs.next_cursor, Some(2.into())); + assert_eq!(actual_epochs_order, [2]); +} + +#[tokio::test] +#[serial] +async fn get_current_epoch() { + let mut sim = Simulacrum::new(); + + execute_simulacrum_transactions(&mut sim, 15); + add_checkpoints(&mut sim, 300); + sim.advance_epoch(false); + + execute_simulacrum_transactions(&mut sim, 10); + add_checkpoints(&mut sim, 300); + sim.advance_epoch(false); + + execute_simulacrum_transactions(&mut sim, 5); + add_checkpoints(&mut sim, 300); + + let last_checkpoint = sim.get_latest_checkpoint().unwrap(); + + let (_, pg_store, _, indexer_client) = + start_simulacrum_rest_api_with_read_write_indexer(Arc::new(sim)).await; + indexer_wait_for_checkpoint(&pg_store, last_checkpoint.sequence_number).await; + + let current_epoch = indexer_client.get_current_epoch().await.unwrap(); + + assert_eq!(current_epoch.epoch, 2); + assert_eq!(current_epoch.first_checkpoint_id, 603); + assert_eq!(current_epoch.epoch_total_transactions, 0); + assert!(current_epoch.end_of_epoch_info.is_none()); +} + +#[ignore = "https://github.com/iotaledger/iota/issues/2197#issuecomment-2371642744"] +#[tokio::test] +#[serial] +async fn get_network_metrics() { + let (_, pg_store, indexer_client) = start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 10).await; + + let network_metrics = indexer_client.get_network_metrics().await.unwrap(); + + println!("{:#?}", network_metrics); +} + +#[ignore = "https://github.com/iotaledger/iota/issues/2197#issuecomment-2371642744"] +#[tokio::test] +#[serial] +async fn get_move_call_metrics() { + let (cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + + execute_move_fn(&cluster).await.unwrap(); + + let latest_checkpoint_sn = cluster + .rpc_client() + .get_latest_checkpoint_sequence_number() + .await + .unwrap(); + indexer_wait_for_checkpoint(&pg_store, latest_checkpoint_sn.into_inner()).await; + + let move_call_metrics = indexer_client.get_move_call_metrics().await.unwrap(); + + // TODO: Why is the move call not included in the stats? + assert_eq!(move_call_metrics.rank_3_days.len(), 0); + assert_eq!(move_call_metrics.rank_7_days.len(), 0); + assert_eq!(move_call_metrics.rank_30_days.len(), 0); +} + +#[ignore = "https://github.com/iotaledger/iota/issues/2197#issuecomment-2371642744"] +#[tokio::test] +#[serial] +async fn get_latest_address_metrics() { + let (_, pg_store, indexer_client) = start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 10).await; + + let address_metrics = indexer_client.get_latest_address_metrics().await.unwrap(); + + println!("{:#?}", address_metrics); +} + +#[ignore = "https://github.com/iotaledger/iota/issues/2197#issuecomment-2371642744"] +#[tokio::test] +#[serial] +async fn get_checkpoint_address_metrics() { + let (_, pg_store, indexer_client) = start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 10).await; + + let address_metrics = indexer_client + .get_checkpoint_address_metrics(0) + .await + .unwrap(); + + println!("{:#?}", address_metrics); +} + +#[ignore = "https://github.com/iotaledger/iota/issues/2197#issuecomment-2371642744"] +#[tokio::test] +#[serial] +async fn get_all_epoch_address_metrics() { + let (_, pg_store, indexer_client) = start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 10).await; + + let address_metrics = indexer_client + .get_all_epoch_address_metrics(None) + .await + .unwrap(); + + println!("{:#?}", address_metrics); +} + +#[tokio::test] +#[serial] +async fn get_total_transactions() { + let mut sim = Simulacrum::new(); + execute_simulacrum_transactions(&mut sim, 5); + + let latest_checkpoint = sim.create_checkpoint(); + let total_transactions_count = latest_checkpoint.network_total_transactions; + + let (_, pg_store, _, indexer_client) = + start_simulacrum_rest_api_with_read_write_indexer(Arc::new(sim)).await; + indexer_wait_for_checkpoint(&pg_store, latest_checkpoint.sequence_number).await; + + let transactions_cnt = indexer_client.get_total_transactions().await.unwrap(); + assert_eq!(transactions_cnt.into_inner(), total_transactions_count); + assert_eq!(transactions_cnt.into_inner(), 6); +} + +async fn execute_move_fn(cluster: &TestCluster) -> Result<(), anyhow::Error> { + let http_client = cluster.rpc_client(); + let address = cluster.get_address_0(); + + let objects = http_client + .get_owned_objects( + address, + Some(IotaObjectResponseQuery::new_with_options( + IotaObjectDataOptions::new() + .with_type() + .with_owner() + .with_previous_transaction(), + )), + None, + None, + ) + .await? + .data; + + let gas = objects.first().unwrap().object().unwrap(); + let coin = &objects[1].object()?; + + // now do the call + let package_id = ObjectID::new(IOTA_FRAMEWORK_ADDRESS.into_bytes()); + let module = "pay".to_string(); + let function = "split".to_string(); + + let transaction_bytes: TransactionBlockBytes = http_client + .move_call( + address, + package_id, + module, + function, + type_args![GAS::type_tag()]?, + call_args!(coin.object_id, 10)?, + Some(gas.object_id), + 10_000_000.into(), + None, + ) + .await?; + + let tx = cluster + .wallet + .sign_transaction(&transaction_bytes.to_data()?); + + let (tx_bytes, signatures) = tx.to_tx_bytes_and_signatures(); + + let tx_response = http_client + .execute_transaction_block( + tx_bytes, + signatures, + Some(IotaTransactionBlockResponseOptions::new().with_effects()), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await?; + assert!(tx_response.status_ok().unwrap_or(false)); + Ok(()) +} + +fn execute_simulacrum_transaction(sim: &mut Simulacrum) { + let transfer_recipient = IotaAddress::random_for_testing_only(); + let (transaction, _) = sim.transfer_txn(transfer_recipient); + sim.execute_transaction(transaction.clone()).unwrap(); +} + +fn execute_simulacrum_transactions(sim: &mut Simulacrum, transactions_count: u32) { + for _ in 0..transactions_count { + execute_simulacrum_transaction(sim); + } +} + +fn add_checkpoints(sim: &mut Simulacrum, checkpoints_count: i32) { + // Main use of this function is to create more checkpoints than the current + // processing batch size, to circumvent the issue described in + // https://github.com/iotaledger/iota/issues/2197#issuecomment-2376432709 + for _ in 0..checkpoints_count { + sim.create_checkpoint(); + } +} diff --git a/crates/iota-indexer/tests/rpc-tests/main.rs b/crates/iota-indexer/tests/rpc-tests/main.rs index 3768cb73acf..d19beb65fda 100644 --- a/crates/iota-indexer/tests/rpc-tests/main.rs +++ b/crates/iota-indexer/tests/rpc-tests/main.rs @@ -5,5 +5,7 @@ #[path = "../common/mod.rs"] mod common; +#[cfg(feature = "pg_integration")] +mod extended_api; #[cfg(feature = "pg_integration")] mod read_api; From 9a1d8ab21fec7862b10e8cf21e5ca557d3131aa9 Mon Sep 17 00:00:00 2001 From: Sergiu Popescu <44298302+sergiupopescu199@users.noreply.github.com> Date: Mon, 30 Sep 2024 14:08:12 +0200 Subject: [PATCH 03/24] feat(iota-indexer): add test for indexer_api (#2927) * feat(iota-indexer): add test for indexer_api * fix(iota-indexer): improve tests * fixup! fix(iota-indexer): improve tests --- .../tests/rpc-tests/indexer_api.rs | 146 ++++++++++++++++++ crates/iota-indexer/tests/rpc-tests/main.rs | 4 + 2 files changed, 150 insertions(+) create mode 100644 crates/iota-indexer/tests/rpc-tests/indexer_api.rs diff --git a/crates/iota-indexer/tests/rpc-tests/indexer_api.rs b/crates/iota-indexer/tests/rpc-tests/indexer_api.rs new file mode 100644 index 00000000000..99f1b4be7c5 --- /dev/null +++ b/crates/iota-indexer/tests/rpc-tests/indexer_api.rs @@ -0,0 +1,146 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::{str::FromStr, time::SystemTime}; + +use iota_json_rpc_api::IndexerApiClient; +use iota_json_rpc_types::{EventFilter, EventPage}; +use iota_types::{ + base_types::{IotaAddress, ObjectID}, + digests::TransactionDigest, +}; +use serial_test::serial; + +use crate::common::pg_integration::{ + indexer_wait_for_checkpoint, rpc_call_error_msg_matches, + start_test_cluster_with_read_write_indexer, +}; + +#[tokio::test] +#[serial] +async fn query_events_no_events_descending() { + let (_cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + + let indexer_events = indexer_client + .query_events( + EventFilter::Sender( + IotaAddress::from_str( + "0x9a934a2644c4ca2decbe3d126d80720429c5e31896aa756765afa23ae2cb4b99", + ) + .unwrap(), + ), + None, + None, + Some(true), + ) + .await + .unwrap(); + + assert_eq!(indexer_events, EventPage::empty()) +} + +#[tokio::test] +#[serial] +async fn query_events_no_events_ascending() { + let (_cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + + let indexer_events = indexer_client + .query_events( + EventFilter::Sender( + IotaAddress::from_str( + "0x9a934a2644c4ca2decbe3d126d80720429c5e31896aa756765afa23ae2cb4b99", + ) + .unwrap(), + ), + None, + None, + None, + ) + .await + .unwrap(); + + assert_eq!(indexer_events, EventPage::empty()) +} + +#[tokio::test] +#[serial] +async fn query_events_unsupported_events() { + let (_cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + + // Get the current time in milliseconds since the UNIX epoch + let now_millis = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_millis(); + + // Subtract 10 minutes from the current time + let ten_minutes_ago = now_millis - (10 * 60 * 1000); // 600 seconds = 10 minutes + + let unsupported_filters = vec![ + EventFilter::All(vec![]), + EventFilter::Any(vec![]), + EventFilter::And( + Box::new(EventFilter::Any(vec![])), + Box::new(EventFilter::Any(vec![])), + ), + EventFilter::Or( + Box::new(EventFilter::Any(vec![])), + Box::new(EventFilter::Any(vec![])), + ), + EventFilter::TimeRange { + start_time: ten_minutes_ago as u64, + end_time: now_millis as u64, + }, + EventFilter::MoveEventField { + path: String::default(), + value: serde_json::Value::Bool(true), + }, + ]; + + for event_filter in unsupported_filters { + let result = indexer_client + .query_events(event_filter, None, None, None) + .await; + + assert!(rpc_call_error_msg_matches( + result, + r#"{"code":-32603,"message": "Indexer does not support the feature with error: `This type of EventFilter is not supported.`"}"#, + )); + } +} + +#[tokio::test] +#[serial] +async fn query_events_supported_events() { + let (_cluster, pg_store, indexer_client) = + start_test_cluster_with_read_write_indexer(None).await; + indexer_wait_for_checkpoint(&pg_store, 1).await; + + let supported_filters = vec![ + EventFilter::Sender(IotaAddress::ZERO), + EventFilter::Transaction(TransactionDigest::ZERO), + EventFilter::Package(ObjectID::ZERO), + EventFilter::MoveEventModule { + package: ObjectID::ZERO, + module: "x".parse().unwrap(), + }, + EventFilter::MoveEventType("0xabcd::MyModule::Foo".parse().unwrap()), + EventFilter::MoveModule { + package: ObjectID::ZERO, + module: "x".parse().unwrap(), + }, + ]; + + for event_filter in supported_filters { + let result = indexer_client + .query_events(event_filter, None, None, None) + .await; + assert!(result.is_ok()); + } +} diff --git a/crates/iota-indexer/tests/rpc-tests/main.rs b/crates/iota-indexer/tests/rpc-tests/main.rs index d19beb65fda..768d52a755f 100644 --- a/crates/iota-indexer/tests/rpc-tests/main.rs +++ b/crates/iota-indexer/tests/rpc-tests/main.rs @@ -7,5 +7,9 @@ mod common; #[cfg(feature = "pg_integration")] mod extended_api; + +#[cfg(feature = "pg_integration")] +mod indexer_api; + #[cfg(feature = "pg_integration")] mod read_api; From c884245d6204a3c62d6dac05c6ca5bf08a2d8e91 Mon Sep 17 00:00:00 2001 From: Sergiu Popescu <44298302+sergiupopescu199@users.noreply.github.com> Date: Tue, 1 Oct 2024 09:20:32 +0200 Subject: [PATCH 04/24] feat(iota-indexer): optimise `rpc_tests` (#2890) * refactor(iota-indexer): explore two impl options for read_api tests * feat(iota-indexer): provide an alternative fn that hides the runtime invoking it under the hood * Revert "feat(iota-indexer): provide an alternative fn that hides the runtime invoking it under the hood" This reverts commit 3a5c9c9f117af1ba9d026b486e089909aeb4fe7d. * fix(iota-indexer): improve the code * feat(iota-indexer): use shared cluster approach and add feature gate * fixup! fix(iota-indexer): improve the code * fixup! fixup! fix(iota-indexer): improve the code --- crates/iota-indexer/Cargo.toml | 1 + crates/iota-indexer/README.md | 11 +- crates/iota-indexer/tests/common/mod.rs | 382 ++-- crates/iota-indexer/tests/ingestion_tests.rs | 3 +- .../tests/rpc-tests/extended_api.rs | 18 +- .../tests/rpc-tests/indexer_api.rs | 2 +- crates/iota-indexer/tests/rpc-tests/main.rs | 4 +- .../iota-indexer/tests/rpc-tests/read_api.rs | 2006 +++++++++-------- 8 files changed, 1268 insertions(+), 1159 deletions(-) diff --git a/crates/iota-indexer/Cargo.toml b/crates/iota-indexer/Cargo.toml index 94d86b9e788..2d58d37510d 100644 --- a/crates/iota-indexer/Cargo.toml +++ b/crates/iota-indexer/Cargo.toml @@ -50,6 +50,7 @@ diesel_migrations = "2.2" [features] pg_integration = [] +shared_test_runtime = [] [dev-dependencies] iota-config.workspace = true diff --git a/crates/iota-indexer/README.md b/crates/iota-indexer/README.md index f6341f80bc9..997dc8496e0 100644 --- a/crates/iota-indexer/README.md +++ b/crates/iota-indexer/README.md @@ -101,13 +101,18 @@ The crate provides following tests currently: > rpc tests which relies on postgres for every test it applies migrations, we need to run tests sequencially to avoid errors ```sh -# run integration tests & rpc tests -cargo test --features pg_integration -- --test-threads 1 +# run tests requiring only postgres integration +cargo test --features pg_integration +# run rpc tests with shared runtime +cargo test --features shared_test_runtime ``` For a better testing experience is possible to use [nextest](https://nexte.st/) +> [!NOTE] +> rpc tests which rely on a shared runtime are not supported with `nextest` + ```sh -# run integration tests & rpc tests +# run tests requiring only postgres integration cargo nextest run --features pg_integration --test-threads 1 ``` diff --git a/crates/iota-indexer/tests/common/mod.rs b/crates/iota-indexer/tests/common/mod.rs index 493642162ea..c3ab57b8634 100644 --- a/crates/iota-indexer/tests/common/mod.rs +++ b/crates/iota-indexer/tests/common/mod.rs @@ -1,190 +1,218 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -#[allow(dead_code)] -#[cfg(feature = "pg_integration")] -pub mod pg_integration { - use std::{net::SocketAddr, sync::Arc, time::Duration}; - - use iota_config::node::RunWithRange; - use iota_indexer::{ - errors::IndexerError, - indexer::Indexer, - store::{indexer_store::IndexerStore, PgIndexerStore}, - test_utils::{start_test_indexer, ReaderWriterConfig}, - IndexerConfig, - }; - use iota_metrics::init_metrics; - use iota_types::storage::ReadStore; - use jsonrpsee::{ - http_client::{HttpClient, HttpClientBuilder}, - types::ErrorObject, - }; - use simulacrum::Simulacrum; - use test_cluster::{TestCluster, TestClusterBuilder}; - use tokio::task::JoinHandle; - - const DEFAULT_DB_URL: &str = "postgres://postgres:postgrespw@localhost:5432/iota_indexer"; - const DEFAULT_INDEXER_IP: &str = "127.0.0.1"; - const DEFAULT_INDEXER_PORT: u16 = 9005; - const DEFAULT_SERVER_PORT: u16 = 3000; - - /// Start a [`TestCluster`][`test_cluster::TestCluster`] with a `Read` & - /// `Write` indexer - pub async fn start_test_cluster_with_read_write_indexer( - stop_cluster_after_checkpoint_seq: Option, - ) -> (TestCluster, PgIndexerStore, HttpClient) { - let mut builder = TestClusterBuilder::new(); - - // run the cluster until the declared checkpoint sequence number - if let Some(stop_cluster_after_checkpoint_seq) = stop_cluster_after_checkpoint_seq { - builder = builder.with_fullnode_run_with_range(Some(RunWithRange::Checkpoint( - stop_cluster_after_checkpoint_seq, - ))); - }; - - let cluster = builder.build().await; - - // start indexer in write mode - let (pg_store, _pg_store_handle) = start_test_indexer( - Some(DEFAULT_DB_URL.to_owned()), - cluster.rpc_url().to_string(), - ReaderWriterConfig::writer_mode(None), - ) - .await; - - // start indexer in read mode - start_indexer_reader(cluster.rpc_url().to_owned()); - - // create an RPC client by using the indexer url - let rpc_client = HttpClientBuilder::default() - .build(format!( - "http://{DEFAULT_INDEXER_IP}:{DEFAULT_INDEXER_PORT}" - )) - .unwrap(); - - (cluster, pg_store, rpc_client) - } +use std::{ + net::SocketAddr, + sync::{Arc, OnceLock}, + time::Duration, +}; + +use iota_config::node::RunWithRange; +use iota_indexer::{ + errors::IndexerError, + indexer::Indexer, + store::{indexer_store::IndexerStore, PgIndexerStore}, + test_utils::{start_test_indexer, ReaderWriterConfig}, + IndexerConfig, +}; +use iota_metrics::init_metrics; +use iota_types::storage::ReadStore; +use jsonrpsee::{ + http_client::{HttpClient, HttpClientBuilder}, + types::ErrorObject, +}; +use simulacrum::Simulacrum; +use test_cluster::{TestCluster, TestClusterBuilder}; +use tokio::{runtime::Runtime, task::JoinHandle}; + +const DEFAULT_DB_URL: &str = "postgres://postgres:postgrespw@localhost:5432/iota_indexer"; +const DEFAULT_INDEXER_IP: &str = "127.0.0.1"; +const DEFAULT_INDEXER_PORT: u16 = 9005; +const DEFAULT_SERVER_PORT: u16 = 3000; + +static GLOBAL_API_TEST_SETUP: OnceLock = OnceLock::new(); + +pub struct ApiTestSetup { + pub runtime: Runtime, + pub cluster: TestCluster, + pub store: PgIndexerStore, + /// Indexer RPC Client + pub client: HttpClient, +} - /// Wait for the indexer to catch up to the given checkpoint sequence number - /// - /// Indexer starts storing data after checkpoint 0 - pub async fn indexer_wait_for_checkpoint( - pg_store: &PgIndexerStore, - checkpoint_sequence_number: u64, - ) { - tokio::time::timeout(Duration::from_secs(10), async { - while { - let cp_opt = pg_store - .get_latest_tx_checkpoint_sequence_number() - .await - .unwrap(); - cp_opt.is_none() || (cp_opt.unwrap() < checkpoint_sequence_number) - } { - tokio::time::sleep(Duration::from_secs(1)).await; - } - }) - .await - .expect("Timeout waiting for indexer to catchup to checkpoint"); - } +impl ApiTestSetup { + pub fn get_or_init() -> &'static ApiTestSetup { + GLOBAL_API_TEST_SETUP.get_or_init(|| { + let runtime = tokio::runtime::Runtime::new().unwrap(); - /// Start an Indexer instance in `Read` mode - fn start_indexer_reader(fullnode_rpc_url: impl Into) { - let config = IndexerConfig { - db_url: Some(DEFAULT_DB_URL.to_owned()), - rpc_client_url: fullnode_rpc_url.into(), - reset_db: true, - rpc_server_worker: true, - rpc_server_url: DEFAULT_INDEXER_IP.to_owned(), - rpc_server_port: DEFAULT_INDEXER_PORT, - ..Default::default() - }; - - let registry = prometheus::Registry::default(); - init_metrics(®istry); - - tokio::spawn(async move { - Indexer::start_reader(&config, ®istry, DEFAULT_DB_URL.to_owned()).await - }); - } + let (cluster, store, client) = + runtime.block_on(start_test_cluster_with_read_write_indexer(None)); - /// Check if provided error message does match with - /// the [`jsonrpsee::core::ClientError::Call`] Error variant - pub fn rpc_call_error_msg_matches( - result: Result, - raw_msg: &str, - ) -> bool { - let err_obj: ErrorObject = serde_json::from_str(raw_msg).unwrap(); - - result.is_err_and(|err| match err { - jsonrpsee::core::ClientError::Call(owned_obj) => { - owned_obj.message() == ErrorObject::into_owned(err_obj).message() + Self { + runtime, + cluster, + store, + client, } - _ => false, }) } +} - pub fn get_default_fullnode_rpc_api_addr() -> SocketAddr { - format!("127.0.0.1:{}", DEFAULT_SERVER_PORT) - .parse() - .unwrap() - } +/// Start a [`TestCluster`][`test_cluster::TestCluster`] with a `Read` & +/// `Write` indexer +pub async fn start_test_cluster_with_read_write_indexer( + stop_cluster_after_checkpoint_seq: Option, +) -> (TestCluster, PgIndexerStore, HttpClient) { + let mut builder = TestClusterBuilder::new(); + + // run the cluster until the declared checkpoint sequence number + if let Some(stop_cluster_after_checkpoint_seq) = stop_cluster_after_checkpoint_seq { + builder = builder.with_fullnode_run_with_range(Some(RunWithRange::Checkpoint( + stop_cluster_after_checkpoint_seq, + ))); + }; - /// Set up a test indexer fetching from a REST endpoint served by the given - /// Simulacrum. - pub async fn start_simulacrum_rest_api_with_write_indexer( - sim: Arc, - ) -> ( - JoinHandle<()>, - PgIndexerStore, - JoinHandle>, - ) { - let server_url = get_default_fullnode_rpc_api_addr(); - - let server_handle = tokio::spawn(async move { - let chain_id = (*sim - .get_checkpoint_by_sequence_number(0) - .unwrap() - .unwrap() - .digest()) - .into(); - - iota_rest_api::RestService::new_without_version(sim, chain_id) - .start_service(server_url, Some("/rest".to_owned())) - .await; - }); - // Starts indexer - let (pg_store, pg_handle) = start_test_indexer( - Some(DEFAULT_DB_URL.to_owned()), - format!("http://{}", server_url), - ReaderWriterConfig::writer_mode(None), - ) - .await; - (server_handle, pg_store, pg_handle) - } + let cluster = builder.build().await; - pub async fn start_simulacrum_rest_api_with_read_write_indexer( - sim: Arc, - ) -> ( - JoinHandle<()>, - PgIndexerStore, - JoinHandle>, - HttpClient, - ) { - let server_url = get_default_fullnode_rpc_api_addr(); - let (server_handle, pg_store, pg_handle) = - start_simulacrum_rest_api_with_write_indexer(sim).await; - - // start indexer in read mode - start_indexer_reader(format!("http://{}", server_url)); - - // create an RPC client by using the indexer url - let rpc_client = HttpClientBuilder::default() - .build(format!( - "http://{DEFAULT_INDEXER_IP}:{DEFAULT_INDEXER_PORT}" - )) - .unwrap(); - - (server_handle, pg_store, pg_handle, rpc_client) - } + // start indexer in write mode + let (pg_store, _pg_store_handle) = start_test_indexer( + Some(DEFAULT_DB_URL.to_owned()), + cluster.rpc_url().to_string(), + ReaderWriterConfig::writer_mode(None), + ) + .await; + + // start indexer in read mode + start_indexer_reader(cluster.rpc_url().to_owned()); + + // create an RPC client by using the indexer url + let rpc_client = HttpClientBuilder::default() + .build(format!( + "http://{DEFAULT_INDEXER_IP}:{DEFAULT_INDEXER_PORT}" + )) + .unwrap(); + + (cluster, pg_store, rpc_client) +} + +/// Wait for the indexer to catch up to the given checkpoint sequence number +/// +/// Indexer starts storing data after checkpoint 0 +pub async fn indexer_wait_for_checkpoint( + pg_store: &PgIndexerStore, + checkpoint_sequence_number: u64, +) { + tokio::time::timeout(Duration::from_secs(30), async { + while { + let cp_opt = pg_store + .get_latest_tx_checkpoint_sequence_number() + .await + .unwrap(); + cp_opt.is_none() || (cp_opt.unwrap() < checkpoint_sequence_number) + } { + tokio::time::sleep(Duration::from_millis(100)).await; + } + }) + .await + .expect("Timeout waiting for indexer to catchup to checkpoint"); +} + +/// Start an Indexer instance in `Read` mode +fn start_indexer_reader(fullnode_rpc_url: impl Into) { + let config = IndexerConfig { + db_url: Some(DEFAULT_DB_URL.to_owned()), + rpc_client_url: fullnode_rpc_url.into(), + reset_db: true, + rpc_server_worker: true, + rpc_server_url: DEFAULT_INDEXER_IP.to_owned(), + rpc_server_port: DEFAULT_INDEXER_PORT, + ..Default::default() + }; + + let registry = prometheus::Registry::default(); + init_metrics(®istry); + + tokio::spawn(async move { + Indexer::start_reader(&config, ®istry, DEFAULT_DB_URL.to_owned()).await + }); +} + +/// Check if provided error message does match with +/// the [`jsonrpsee::core::ClientError::Call`] Error variant +pub fn rpc_call_error_msg_matches( + result: Result, + raw_msg: &str, +) -> bool { + let err_obj: ErrorObject = serde_json::from_str(raw_msg).unwrap(); + + result.is_err_and(|err| match err { + jsonrpsee::core::ClientError::Call(owned_obj) => { + owned_obj.message() == ErrorObject::into_owned(err_obj).message() + } + _ => false, + }) +} + +pub fn get_default_fullnode_rpc_api_addr() -> SocketAddr { + format!("127.0.0.1:{}", DEFAULT_SERVER_PORT) + .parse() + .unwrap() +} + +/// Set up a test indexer fetching from a REST endpoint served by the given +/// Simulacrum. +pub async fn start_simulacrum_rest_api_with_write_indexer( + sim: Arc, +) -> ( + JoinHandle<()>, + PgIndexerStore, + JoinHandle>, +) { + let server_url = get_default_fullnode_rpc_api_addr(); + + let server_handle = tokio::spawn(async move { + let chain_id = (*sim + .get_checkpoint_by_sequence_number(0) + .unwrap() + .unwrap() + .digest()) + .into(); + + iota_rest_api::RestService::new_without_version(sim, chain_id) + .start_service(server_url, Some("/rest".to_owned())) + .await; + }); + // Starts indexer + let (pg_store, pg_handle) = start_test_indexer( + Some(DEFAULT_DB_URL.to_owned()), + format!("http://{}", server_url), + ReaderWriterConfig::writer_mode(None), + ) + .await; + (server_handle, pg_store, pg_handle) +} + +pub async fn start_simulacrum_rest_api_with_read_write_indexer( + sim: Arc, +) -> ( + JoinHandle<()>, + PgIndexerStore, + JoinHandle>, + HttpClient, +) { + let server_url = get_default_fullnode_rpc_api_addr(); + let (server_handle, pg_store, pg_handle) = + start_simulacrum_rest_api_with_write_indexer(sim).await; + + // start indexer in read mode + start_indexer_reader(format!("http://{}", server_url)); + + // create an RPC client by using the indexer url + let rpc_client = HttpClientBuilder::default() + .build(format!( + "http://{DEFAULT_INDEXER_IP}:{DEFAULT_INDEXER_PORT}" + )) + .unwrap(); + + (server_handle, pg_store, pg_handle, rpc_client) } diff --git a/crates/iota-indexer/tests/ingestion_tests.rs b/crates/iota-indexer/tests/ingestion_tests.rs index f04f9a1f13d..4488e27db5e 100644 --- a/crates/iota-indexer/tests/ingestion_tests.rs +++ b/crates/iota-indexer/tests/ingestion_tests.rs @@ -1,6 +1,7 @@ // Copyright (c) Mysten Labs, Inc. // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +#[allow(dead_code)] #[cfg(feature = "pg_integration")] mod common; #[cfg(feature = "pg_integration")] @@ -17,7 +18,7 @@ mod ingestion_tests { use iota_types::{base_types::IotaAddress, effects::TransactionEffectsAPI}; use simulacrum::Simulacrum; - use crate::common::pg_integration::{ + use crate::common::{ indexer_wait_for_checkpoint, start_simulacrum_rest_api_with_write_indexer, }; diff --git a/crates/iota-indexer/tests/rpc-tests/extended_api.rs b/crates/iota-indexer/tests/rpc-tests/extended_api.rs index 85ae15a12c0..7016b705d7d 100644 --- a/crates/iota-indexer/tests/rpc-tests/extended_api.rs +++ b/crates/iota-indexer/tests/rpc-tests/extended_api.rs @@ -22,7 +22,7 @@ use serial_test::serial; use simulacrum::Simulacrum; use test_cluster::TestCluster; -use crate::common::pg_integration::{ +use crate::common::{ indexer_wait_for_checkpoint, start_simulacrum_rest_api_with_read_write_indexer, start_test_cluster_with_read_write_indexer, }; @@ -52,7 +52,7 @@ async fn get_epochs() { let epochs = indexer_client.get_epochs(None, None, None).await.unwrap(); assert_eq!(epochs.data.len(), 3); - assert_eq!(epochs.has_next_page, false); + assert!(!epochs.has_next_page); let end_of_epoch_info = epochs.data[0].end_of_epoch_info.as_ref().unwrap(); assert_eq!(epochs.data[0].epoch, 0); @@ -106,7 +106,7 @@ async fn get_epochs_descending() { .collect::>(); assert_eq!(epochs.data.len(), 3); - assert_eq!(epochs.has_next_page, false); + assert!(!epochs.has_next_page); assert_eq!(actual_epochs_order, [2, 1, 0]) } @@ -143,7 +143,7 @@ async fn get_epochs_paging() { .collect::>(); assert_eq!(epochs.data.len(), 2); - assert_eq!(epochs.has_next_page, true); + assert!(epochs.has_next_page); assert_eq!(epochs.next_cursor, Some(1.into())); assert_eq!(actual_epochs_order, [0, 1]); @@ -158,7 +158,7 @@ async fn get_epochs_paging() { .collect::>(); assert_eq!(epochs.data.len(), 1); - assert_eq!(epochs.has_next_page, false); + assert!(!epochs.has_next_page); assert_eq!(epochs.next_cursor, Some(2.into())); assert_eq!(actual_epochs_order, [2]); } @@ -191,7 +191,7 @@ async fn get_epoch_metrics() { .unwrap(); assert_eq!(epoch_metrics.data.len(), 3); - assert_eq!(epoch_metrics.has_next_page, false); + assert!(!epoch_metrics.has_next_page); let end_of_epoch_info = epoch_metrics.data[0].end_of_epoch_info.as_ref().unwrap(); assert_eq!(epoch_metrics.data[0].epoch, 0); @@ -245,7 +245,7 @@ async fn get_epoch_metrics_descending() { .collect::>(); assert_eq!(epochs.data.len(), 3); - assert_eq!(epochs.has_next_page, false); + assert!(!epochs.has_next_page); assert_eq!(actual_epochs_order, [2, 1, 0]) } @@ -282,7 +282,7 @@ async fn get_epoch_metrics_paging() { .collect::>(); assert_eq!(epochs.data.len(), 2); - assert_eq!(epochs.has_next_page, true); + assert!(epochs.has_next_page); assert_eq!(epochs.next_cursor, Some(1.into())); assert_eq!(actual_epochs_order, [0, 1]); @@ -297,7 +297,7 @@ async fn get_epoch_metrics_paging() { .collect::>(); assert_eq!(epochs.data.len(), 1); - assert_eq!(epochs.has_next_page, false); + assert!(!epochs.has_next_page); assert_eq!(epochs.next_cursor, Some(2.into())); assert_eq!(actual_epochs_order, [2]); } diff --git a/crates/iota-indexer/tests/rpc-tests/indexer_api.rs b/crates/iota-indexer/tests/rpc-tests/indexer_api.rs index 99f1b4be7c5..1f93b04d472 100644 --- a/crates/iota-indexer/tests/rpc-tests/indexer_api.rs +++ b/crates/iota-indexer/tests/rpc-tests/indexer_api.rs @@ -11,7 +11,7 @@ use iota_types::{ }; use serial_test::serial; -use crate::common::pg_integration::{ +use crate::common::{ indexer_wait_for_checkpoint, rpc_call_error_msg_matches, start_test_cluster_with_read_write_indexer, }; diff --git a/crates/iota-indexer/tests/rpc-tests/main.rs b/crates/iota-indexer/tests/rpc-tests/main.rs index 768d52a755f..df7236bdbda 100644 --- a/crates/iota-indexer/tests/rpc-tests/main.rs +++ b/crates/iota-indexer/tests/rpc-tests/main.rs @@ -1,7 +1,7 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -#[cfg(feature = "pg_integration")] +#[allow(dead_code)] #[path = "../common/mod.rs"] mod common; @@ -11,5 +11,5 @@ mod extended_api; #[cfg(feature = "pg_integration")] mod indexer_api; -#[cfg(feature = "pg_integration")] +#[cfg(feature = "shared_test_runtime")] mod read_api; diff --git a/crates/iota-indexer/tests/rpc-tests/read_api.rs b/crates/iota-indexer/tests/rpc-tests/read_api.rs index b3c1d989814..61c74e0f74b 100644 --- a/crates/iota-indexer/tests/rpc-tests/read_api.rs +++ b/crates/iota-indexer/tests/rpc-tests/read_api.rs @@ -3,7 +3,6 @@ use std::str::FromStr; -use iota_config::node::RunWithRange; use iota_json_rpc_api::{IndexerApiClient, ReadApiClient}; use iota_json_rpc_types::{ CheckpointId, IotaGetPastObjectRequest, IotaObjectDataOptions, IotaObjectResponse, @@ -14,12 +13,8 @@ use iota_types::{ digests::TransactionDigest, error::IotaObjectResponseError, }; -use serial_test::serial; -use crate::common::pg_integration::{ - indexer_wait_for_checkpoint, rpc_call_error_msg_matches, - start_test_cluster_with_read_write_indexer, -}; +use crate::common::{indexer_wait_for_checkpoint, rpc_call_error_msg_matches, ApiTestSetup}; fn is_ascending(vec: &[u64]) -> bool { vec.windows(2).all(|window| window[0] <= window[1]) @@ -29,7 +24,8 @@ fn is_descending(vec: &[u64]) -> bool { } /// Checks if -/// [`iota_json_rpc_types::IotaTransactionBlockResponse`] match to the provided +/// [`iota_json_rpc_types::IotaTransactionBlockResponse`] match to the +/// provided /// [`iota_json_rpc_types::IotaTransactionBlockResponseOptions`] filters fn match_transaction_block_resp_options( expected_options: &IotaTransactionBlockResponseOptions, @@ -49,1167 +45,1245 @@ fn match_transaction_block_resp_options( .all(|actual_options| actual_options.eq(expected_options)) } -async fn get_object_with_options(options: IotaObjectDataOptions) { - let (cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; - let address = cluster.get_address_0(); - - let fullnode_objects = cluster - .rpc_client() - .get_owned_objects( - address, - Some(IotaObjectResponseQuery::new_with_options(options.clone())), - None, - None, - ) - .await - .unwrap(); - - for obj in fullnode_objects.data { - let indexer_obj = indexer_client - .get_object(obj.object_id().unwrap(), Some(options.clone())) +fn get_object_with_options(options: IotaObjectDataOptions) { + let ApiTestSetup { + runtime, + cluster, + store, + client, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + let address = cluster.get_address_0(); + + let fullnode_objects = cluster + .rpc_client() + .get_owned_objects( + address, + Some(IotaObjectResponseQuery::new_with_options(options.clone())), + None, + None, + ) .await .unwrap(); - assert_eq!(obj, indexer_obj); - } -} - -async fn multi_get_objects_with_options(options: IotaObjectDataOptions) { - let (cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; - let address = cluster.get_address_0(); + for obj in fullnode_objects.data { + let indexer_obj = client + .get_object(obj.object_id().unwrap(), Some(options.clone())) + .await + .unwrap(); - let fullnode_objects = cluster - .rpc_client() - .get_owned_objects( - address, - Some(IotaObjectResponseQuery::new_with_options(options.clone())), - None, - None, - ) - .await - .unwrap(); + assert_eq!(obj, indexer_obj); + } + }); +} + +fn multi_get_objects_with_options(options: IotaObjectDataOptions) { + let ApiTestSetup { + runtime, + cluster, + store, + client, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + let address = cluster.get_address_0(); + + let fullnode_objects = cluster + .rpc_client() + .get_owned_objects( + address, + Some(IotaObjectResponseQuery::new_with_options(options.clone())), + None, + None, + ) + .await + .unwrap(); - let object_ids = fullnode_objects - .data - .iter() - .map(|iota_object| iota_object.object_id().unwrap()) - .collect::>(); + let object_ids = fullnode_objects + .data + .iter() + .map(|iota_object| iota_object.object_id().unwrap()) + .collect::>(); - let indexer_objects = indexer_client - .multi_get_objects(object_ids, Some(options)) - .await - .unwrap(); + let indexer_objects = client + .multi_get_objects(object_ids, Some(options)) + .await + .unwrap(); - assert_eq!(fullnode_objects.data, indexer_objects); + assert_eq!(fullnode_objects.data, indexer_objects); + }); } -async fn get_transaction_block_with_options(options: IotaTransactionBlockResponseOptions) { - let (cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; +fn get_transaction_block_with_options(options: IotaTransactionBlockResponseOptions) { + let ApiTestSetup { + runtime, + cluster, + store, + client, + } = ApiTestSetup::get_or_init(); - let fullnode_checkpoint = cluster - .rpc_client() - .get_checkpoint(CheckpointId::SequenceNumber(0)) - .await - .unwrap(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; - let tx_digest = *fullnode_checkpoint.transactions.first().unwrap(); - - let fullnode_tx = cluster - .rpc_client() - .get_transaction_block(tx_digest, Some(options.clone())) - .await - .unwrap(); + let fullnode_checkpoint = cluster + .rpc_client() + .get_checkpoint(CheckpointId::SequenceNumber(0)) + .await + .unwrap(); - let tx = indexer_client - .get_transaction_block(tx_digest, Some(options.clone())) - .await - .unwrap(); + let tx_digest = *fullnode_checkpoint.transactions.first().unwrap(); - // `IotaTransactionBlockResponse` does have a custom PartialEq impl which does - // not match all options filters but is still good to check if both tx does - // match - assert_eq!(fullnode_tx, tx); + let fullnode_tx = cluster + .rpc_client() + .get_transaction_block(tx_digest, Some(options.clone())) + .await + .unwrap(); - assert!( - match_transaction_block_resp_options(&options, &[fullnode_tx]), - "fullnode transaction block assertion failed" - ); - assert!( - match_transaction_block_resp_options(&options, &[tx]), - "indexer transaction block assertion failed" - ); -} + let tx = client + .get_transaction_block(tx_digest, Some(options.clone())) + .await + .unwrap(); -async fn multi_get_transaction_blocks_with_options(options: IotaTransactionBlockResponseOptions) { - let (cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 3).await; - - let fullnode_checkpoints = cluster - .rpc_client() - .get_checkpoints(None, Some(3), false) - .await - .unwrap(); - - let digests = fullnode_checkpoints - .data - .into_iter() - .flat_map(|c| c.transactions) - .collect::>(); - - let fullnode_txs = cluster - .rpc_client() - .multi_get_transaction_blocks(digests.clone(), Some(options.clone())) - .await - .unwrap(); - - let indexer_txs = indexer_client - .multi_get_transaction_blocks(digests, Some(options.clone())) - .await - .unwrap(); - - // `IotaTransactionBlockResponse` does have a custom PartialEq impl which does - // not match all options filters but is still good to check if both tx does - // match - assert_eq!(fullnode_txs, indexer_txs); - - assert!( - match_transaction_block_resp_options(&options, &fullnode_txs), - "fullnode multi transaction blocks assertion failed" - ); - assert!( - match_transaction_block_resp_options(&options, &indexer_txs), - "indexer multi transaction blocks assertion failed" - ); -} + // `IotaTransactionBlockResponse` does have a custom PartialEq impl which does + // not match all options filters but is still good to check if both tx does + // match + assert_eq!(fullnode_tx, tx); + + assert!( + match_transaction_block_resp_options(&options, &[fullnode_tx]), + "fullnode transaction block assertion failed" + ); + assert!( + match_transaction_block_resp_options(&options, &[tx]), + "indexer transaction block assertion failed" + ); + }); +} + +fn multi_get_transaction_blocks_with_options(options: IotaTransactionBlockResponseOptions) { + let ApiTestSetup { + runtime, + cluster, + store, + client, + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 3).await; + + let fullnode_checkpoints = cluster + .rpc_client() + .get_checkpoints(None, Some(3), false) + .await + .unwrap(); -#[tokio::test] -#[serial] -async fn get_checkpoint_by_seq_num() { - let (cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; - - let fullnode_checkpoint = cluster - .rpc_client() - .get_checkpoint(CheckpointId::SequenceNumber(0)) - .await - .unwrap(); - - let indexer_checkpoint = indexer_client - .get_checkpoint(CheckpointId::SequenceNumber(0)) - .await - .unwrap(); - - assert_eq!(fullnode_checkpoint, indexer_checkpoint); -} - -#[tokio::test] -#[serial] -async fn get_checkpoint_by_seq_num_not_found() { - let (_cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; - - let result = indexer_client - .get_checkpoint(CheckpointId::SequenceNumber(100000000000)) - .await; - - assert!(rpc_call_error_msg_matches( - result, - r#"{"code":-32603,"message":"Invalid argument with error: `Checkpoint SequenceNumber(100000000000) not found`"}"#, - )); -} - -#[tokio::test] -#[serial] -async fn get_checkpoint_by_digest() { - let (cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; - - let fullnode_checkpoint = cluster - .rpc_client() - .get_checkpoint(CheckpointId::SequenceNumber(0)) - .await - .unwrap(); - - let indexer_checkpoint = indexer_client - .get_checkpoint(CheckpointId::Digest(fullnode_checkpoint.digest)) - .await - .unwrap(); - - assert_eq!(fullnode_checkpoint, indexer_checkpoint); -} - -#[tokio::test] -#[serial] -async fn get_checkpoint_by_digest_not_found() { - let (_cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; - - let result = indexer_client - .get_checkpoint(CheckpointId::Digest([0; 32].into())) - .await; - - assert!(rpc_call_error_msg_matches( - result, - r#"{"code":-32603,"message":"Invalid argument with error: `Checkpoint Digest(CheckpointDigest(11111111111111111111111111111111)) not found`"}"#, - )); -} - -#[tokio::test] -#[serial] -async fn get_checkpoints_all_ascending() { - let (_cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 3).await; - - let indexer_checkpoint = indexer_client - .get_checkpoints(None, None, false) - .await - .unwrap(); - - let seq_numbers = indexer_checkpoint - .data - .iter() - .map(|c| c.sequence_number) - .collect::>(); + let digests = fullnode_checkpoints + .data + .into_iter() + .flat_map(|c| c.transactions) + .collect::>(); - assert!(is_ascending(&seq_numbers)); -} + let fullnode_txs = cluster + .rpc_client() + .multi_get_transaction_blocks(digests.clone(), Some(options.clone())) + .await + .unwrap(); -#[tokio::test] -#[serial] -async fn get_checkpoints_all_descending() { - let (_cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 3).await; + let indexer_txs = client + .multi_get_transaction_blocks(digests, Some(options.clone())) + .await + .unwrap(); - let indexer_checkpoint = indexer_client - .get_checkpoints(None, None, true) - .await - .unwrap(); + // `IotaTransactionBlockResponse` does have a custom PartialEq impl which does + // not match all options filters but is still good to check if both tx does + // match + assert_eq!(fullnode_txs, indexer_txs); + + assert!( + match_transaction_block_resp_options(&options, &fullnode_txs), + "fullnode multi transaction blocks assertion failed" + ); + assert!( + match_transaction_block_resp_options(&options, &indexer_txs), + "indexer multi transaction blocks assertion failed" + ); + }); +} + +#[test] +fn get_checkpoint_by_seq_num() { + let ApiTestSetup { + runtime, + cluster, + store, + client, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let fullnode_checkpoint = cluster + .rpc_client() + .get_checkpoint(CheckpointId::SequenceNumber(0)) + .await + .unwrap(); - let seq_numbers = indexer_checkpoint - .data - .iter() - .map(|c| c.sequence_number) - .collect::>(); + let indexer_checkpoint = client + .get_checkpoint(CheckpointId::SequenceNumber(0)) + .await + .unwrap(); - assert!(is_descending(&seq_numbers)); + assert_eq!(fullnode_checkpoint, indexer_checkpoint); + }) } -#[tokio::test] -#[serial] -async fn get_checkpoints_by_cursor_and_limit_one_descending() { - let (_cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 3).await; +#[test] +fn get_checkpoint_by_seq_num_not_found() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let result = client + .get_checkpoint(CheckpointId::SequenceNumber(100000000000)) + .await; + assert!(rpc_call_error_msg_matches( + result, + r#"{"code":-32603,"message":"Invalid argument with error: `Checkpoint SequenceNumber(100000000000) not found`"}"#, + )); + }); +} + +#[test] +fn get_checkpoint_by_digest() { + let ApiTestSetup { + runtime, + cluster, + store, + client, + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let fullnode_checkpoint = cluster + .rpc_client() + .get_checkpoint(CheckpointId::SequenceNumber(0)) + .await + .unwrap(); - let indexer_checkpoint = indexer_client - .get_checkpoints(Some(1.into()), Some(1), true) - .await - .unwrap(); + let indexer_checkpoint = client + .get_checkpoint(CheckpointId::Digest(fullnode_checkpoint.digest)) + .await + .unwrap(); - assert_eq!( - vec![0], - indexer_checkpoint - .data - .into_iter() - .map(|c| c.sequence_number) - .collect::>() - ); + assert_eq!(fullnode_checkpoint, indexer_checkpoint); + }); } -#[tokio::test] -#[serial] -async fn get_checkpoints_by_cursor_and_limit_one_ascending() { - let (_cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 3).await; +#[test] +fn get_checkpoint_by_digest_not_found() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); - let indexer_checkpoint = indexer_client - .get_checkpoints(Some(1.into()), Some(1), false) - .await - .unwrap(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; - assert_eq!( - vec![2], - indexer_checkpoint - .data - .into_iter() - .map(|c| c.sequence_number) - .collect::>() - ); -} - -#[tokio::test] -#[serial] -async fn get_checkpoints_by_cursor_zero_and_limit_ascending() { - let (_cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 3).await; - - let indexer_checkpoint = indexer_client - .get_checkpoints(Some(0.into()), Some(3), false) - .await - .unwrap(); + let result = client + .get_checkpoint(CheckpointId::Digest([0; 32].into())) + .await; - assert_eq!( - vec![1, 2, 3], - indexer_checkpoint - .data - .into_iter() - .map(|c| c.sequence_number) - .collect::>() - ); + assert!(rpc_call_error_msg_matches( + result, + r#"{"code":-32603,"message":"Invalid argument with error: `Checkpoint Digest(CheckpointDigest(11111111111111111111111111111111)) not found`"}"#, + )); + }); } -#[tokio::test] -#[serial] -async fn get_checkpoints_by_cursor_zero_and_limit_descending() { - let (_cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 3).await; - - let indexer_checkpoint = indexer_client - .get_checkpoints(Some(0.into()), Some(3), true) - .await - .unwrap(); - - assert_eq!( - Vec::::default(), - indexer_checkpoint - .data - .into_iter() - .map(|c| c.sequence_number) - .collect::>() - ); -} +#[test] +fn get_checkpoints_all_ascending() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); -#[tokio::test] -#[serial] -async fn get_checkpoints_by_cursor_and_limit_ascending() { - let (_cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 6).await; + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 3).await; - let indexer_checkpoint = indexer_client - .get_checkpoints(Some(3.into()), Some(3), false) - .await - .unwrap(); + let indexer_checkpoint = client.get_checkpoints(None, None, false).await.unwrap(); - assert_eq!( - vec![4, 5, 6], - indexer_checkpoint + let seq_numbers = indexer_checkpoint .data - .into_iter() + .iter() .map(|c| c.sequence_number) - .collect::>() - ); + .collect::>(); + + assert!(is_ascending(&seq_numbers)); + }); } -#[tokio::test] -#[serial] -async fn get_checkpoints_by_cursor_and_limit_descending() { - let (_cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 3).await; +#[test] +fn get_checkpoints_all_descending() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 3).await; - let indexer_checkpoint = indexer_client - .get_checkpoints(Some(3.into()), Some(3), true) - .await - .unwrap(); + let indexer_checkpoint = client.get_checkpoints(None, None, true).await.unwrap(); - assert_eq!( - vec![2, 1, 0], - indexer_checkpoint + let seq_numbers = indexer_checkpoint .data - .into_iter() + .iter() .map(|c| c.sequence_number) - .collect::>() - ); + .collect::>(); + + assert!(is_descending(&seq_numbers)); + }); } -#[tokio::test] -#[serial] -async fn get_checkpoints_invalid_limit() { - let (_cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 3).await; +#[test] +fn get_checkpoints_by_cursor_and_limit_one_descending() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 3).await; - let result = indexer_client.get_checkpoints(None, Some(0), false).await; + let indexer_checkpoint = client + .get_checkpoints(Some(1.into()), Some(1), true) + .await + .unwrap(); - assert!(rpc_call_error_msg_matches( - result, - r#"{"code":-32602,"message":"Page size limit cannot be smaller than 1"}"#, - )); -} + assert_eq!( + vec![0], + indexer_checkpoint + .data + .into_iter() + .map(|c| c.sequence_number) + .collect::>() + ); + }); +} + +#[test] +fn get_checkpoints_by_cursor_and_limit_one_ascending() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 3).await; + + let indexer_checkpoint = client + .get_checkpoints(Some(1.into()), Some(1), false) + .await + .unwrap(); -#[tokio::test] -#[serial] -async fn get_object() { - let (cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; - let address = cluster.get_address_0(); + assert_eq!( + vec![2], + indexer_checkpoint + .data + .into_iter() + .map(|c| c.sequence_number) + .collect::>() + ); + }); +} + +#[test] +fn get_checkpoints_by_cursor_zero_and_limit_ascending() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 3).await; + + let indexer_checkpoint = client + .get_checkpoints(Some(0.into()), Some(3), false) + .await + .unwrap(); - let fullnode_objects = cluster - .rpc_client() - .get_owned_objects(address, None, None, None) - .await - .unwrap(); + assert_eq!( + vec![1, 2, 3], + indexer_checkpoint + .data + .into_iter() + .map(|c| c.sequence_number) + .collect::>() + ); + }); +} + +#[test] +fn get_checkpoints_by_cursor_zero_and_limit_descending() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 3).await; + + let indexer_checkpoint = client + .get_checkpoints(Some(0.into()), Some(3), true) + .await + .unwrap(); - for obj in fullnode_objects.data { - let indexer_obj = indexer_client - .get_object(obj.object_id().unwrap(), None) + assert_eq!( + Vec::::default(), + indexer_checkpoint + .data + .into_iter() + .map(|c| c.sequence_number) + .collect::>() + ); + }); +} + +#[test] +fn get_checkpoints_by_cursor_and_limit_ascending() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 6).await; + + let indexer_checkpoint = client + .get_checkpoints(Some(3.into()), Some(3), false) .await .unwrap(); - assert_eq!(obj, indexer_obj) - } -} -#[tokio::test] -#[serial] -async fn get_object_not_found() { - let (_cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; + assert_eq!( + vec![4, 5, 6], + indexer_checkpoint + .data + .into_iter() + .map(|c| c.sequence_number) + .collect::>() + ); + }); +} + +#[test] +fn get_checkpoints_by_cursor_and_limit_descending() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 3).await; + + let indexer_checkpoint = client + .get_checkpoints(Some(3.into()), Some(3), true) + .await + .unwrap(); - let indexer_obj = indexer_client - .get_object( - ObjectID::from_str( - "0x9a934a2644c4ca2decbe3d126d80720429c5e31896aa756765afa23ae2cb4b99", + assert_eq!( + // vec![2, 1, 5], + vec![2, 1, 0], + indexer_checkpoint + .data + .into_iter() + .map(|c| c.sequence_number) + .collect::>() + ); + }); +} + +#[test] +fn get_checkpoints_invalid_limit() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 3).await; + + let result = client.get_checkpoints(None, Some(0), false).await; + + assert!(rpc_call_error_msg_matches( + result, + r#"{"code":-32602,"message":"Page size limit cannot be smaller than 1"}"#, + )); + }); +} + +#[test] +fn get_object() { + let ApiTestSetup { + runtime, + cluster, + store, + client, + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + let address = cluster.get_address_0(); + + let fullnode_objects = cluster + .rpc_client() + .get_owned_objects(address, None, None, None) + .await + .unwrap(); + + for obj in fullnode_objects.data { + let indexer_obj = client + .get_object(obj.object_id().unwrap(), None) + .await + .unwrap(); + assert_eq!(obj, indexer_obj) + } + }); +} + +#[test] +fn get_object_not_found() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let indexer_obj = client + .get_object( + ObjectID::from_str( + "0x9a934a2644c4ca2decbe3d126d80720429c5e31896aa756765afa23ae2cb4b99", + ) + .unwrap(), + None, ) - .unwrap(), - None, + .await + .unwrap(); + + assert_eq!( + indexer_obj, + IotaObjectResponse { + data: None, + error: Some(IotaObjectResponseError::NotExists { + object_id: "0x9a934a2644c4ca2decbe3d126d80720429c5e31896aa756765afa23ae2cb4b99" + .parse() + .unwrap() + }) + } ) - .await - .unwrap(); - - assert_eq!( - indexer_obj, - IotaObjectResponse { - data: None, - error: Some(IotaObjectResponseError::NotExists { - object_id: "0x9a934a2644c4ca2decbe3d126d80720429c5e31896aa756765afa23ae2cb4b99" - .parse() - .unwrap() - }) - } - ) + }); } -#[tokio::test] -#[serial] -async fn get_object_with_bcs_lossless() { - get_object_with_options(IotaObjectDataOptions::bcs_lossless()).await; +#[test] +fn get_object_with_bcs_lossless() { + get_object_with_options(IotaObjectDataOptions::bcs_lossless()); } -#[tokio::test] -#[serial] -async fn get_object_with_full_content() { - get_object_with_options(IotaObjectDataOptions::full_content()).await; +#[test] +fn get_object_with_full_content() { + get_object_with_options(IotaObjectDataOptions::full_content()); } -#[tokio::test] -#[serial] -async fn get_object_with_bcs() { - get_object_with_options(IotaObjectDataOptions::default().with_bcs()).await; +#[test] +fn get_object_with_bcs() { + get_object_with_options(IotaObjectDataOptions::default().with_bcs()); } -#[tokio::test] -#[serial] -async fn get_object_with_content() { - get_object_with_options(IotaObjectDataOptions::default().with_content()).await; +#[test] +fn get_object_with_content() { + get_object_with_options(IotaObjectDataOptions::default().with_content()); } -#[tokio::test] -#[serial] -async fn get_object_with_display() { - get_object_with_options(IotaObjectDataOptions::default().with_display()).await; +#[test] +fn get_object_with_display() { + get_object_with_options(IotaObjectDataOptions::default().with_display()); } -#[tokio::test] -#[serial] -async fn get_object_with_owner() { - get_object_with_options(IotaObjectDataOptions::default().with_owner()).await; +#[test] +fn get_object_with_owner() { + get_object_with_options(IotaObjectDataOptions::default().with_owner()); } -#[tokio::test] -#[serial] -async fn get_object_with_previous_transaction() { - get_object_with_options(IotaObjectDataOptions::default().with_previous_transaction()).await; +#[test] +fn get_object_with_previous_transaction() { + get_object_with_options(IotaObjectDataOptions::default().with_previous_transaction()); } -#[tokio::test] -#[serial] -async fn get_object_with_type() { - get_object_with_options(IotaObjectDataOptions::default().with_type()).await; +#[test] +fn get_object_with_type() { + get_object_with_options(IotaObjectDataOptions::default().with_type()); } -#[tokio::test] -#[serial] -async fn get_object_with_storage_rebate() { +#[test] +fn get_object_with_storage_rebate() { get_object_with_options(IotaObjectDataOptions { show_storage_rebate: true, ..Default::default() - }) - .await; -} - -#[tokio::test] -#[serial] -async fn multi_get_objects() { - let (cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; - let address = cluster.get_address_0(); - - let fullnode_objects = cluster - .rpc_client() - .get_owned_objects(address, None, None, None) - .await - .unwrap(); + }); +} + +#[test] +fn multi_get_objects() { + let ApiTestSetup { + runtime, + cluster, + store, + client, + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + let address = cluster.get_address_0(); + + let fullnode_objects = cluster + .rpc_client() + .get_owned_objects(address, None, None, None) + .await + .unwrap(); - let object_ids = fullnode_objects - .data - .iter() - .map(|iota_object| iota_object.object_id().unwrap()) - .collect(); + let object_ids = fullnode_objects + .data + .iter() + .map(|iota_object| iota_object.object_id().unwrap()) + .collect(); - let indexer_objects = indexer_client - .multi_get_objects(object_ids, None) - .await - .unwrap(); + let indexer_objects = client.multi_get_objects(object_ids, None).await.unwrap(); - assert_eq!(fullnode_objects.data, indexer_objects); + assert_eq!(fullnode_objects.data, indexer_objects); + }); } -#[tokio::test] -#[serial] -async fn multi_get_objects_not_found() { - let (_cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; +#[test] +fn multi_get_objects_not_found() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; - let object_ids = vec![ - ObjectID::from_str("0x9a934a2644c4ca2decbe3d126d80720429c5e31896aa756765afa23ae2cb4b99") + let object_ids = vec![ + ObjectID::from_str( + "0x9a934a2644c4ca2decbe3d126d80720429c5e31896aa756765afa23ae2cb4b99", + ) .unwrap(), - ObjectID::from_str("0x1a934a7644c4cf2decbe3d126d80720429c5e30896aa756765afa23af3cb4b82") + ObjectID::from_str( + "0x1a934a7644c4cf2decbe3d126d80720429c5e30896aa756765afa23af3cb4b82", + ) .unwrap(), - ]; - - let indexer_objects = indexer_client - .multi_get_objects(object_ids, None) - .await - .unwrap(); + ]; + + let indexer_objects = client.multi_get_objects(object_ids, None).await.unwrap(); + + assert_eq!( + indexer_objects, + vec![ + IotaObjectResponse { + data: None, + error: Some(IotaObjectResponseError::NotExists { + object_id: + "0x9a934a2644c4ca2decbe3d126d80720429c5e31896aa756765afa23ae2cb4b99" + .parse() + .unwrap() + }) + }, + IotaObjectResponse { + data: None, + error: Some(IotaObjectResponseError::NotExists { + object_id: + "0x1a934a7644c4cf2decbe3d126d80720429c5e30896aa756765afa23af3cb4b82" + .parse() + .unwrap() + }) + } + ] + ) + }); +} + +#[test] +fn multi_get_objects_found_and_not_found() { + let ApiTestSetup { + runtime, + cluster, + store, + client, + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + let address = cluster.get_address_0(); + + let fullnode_objects = cluster + .rpc_client() + .get_owned_objects(address, None, None, None) + .await + .unwrap(); - assert_eq!( - indexer_objects, - vec![ - IotaObjectResponse { - data: None, - error: Some(IotaObjectResponseError::NotExists { - object_id: "0x9a934a2644c4ca2decbe3d126d80720429c5e31896aa756765afa23ae2cb4b99" - .parse() - .unwrap() - }) - }, - IotaObjectResponse { - data: None, - error: Some(IotaObjectResponseError::NotExists { - object_id: "0x1a934a7644c4cf2decbe3d126d80720429c5e30896aa756765afa23af3cb4b82" - .parse() - .unwrap() - }) - } - ] - ) -} - -#[tokio::test] -#[serial] -async fn multi_get_objects_found_and_not_found() { - let (cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; - let address = cluster.get_address_0(); - - let fullnode_objects = cluster - .rpc_client() - .get_owned_objects(address, None, None, None) - .await - .unwrap(); - - let mut object_ids = fullnode_objects - .data - .iter() - .map(|iota_object| iota_object.object_id().unwrap()) - .collect::>(); + let mut object_ids = fullnode_objects + .data + .iter() + .map(|iota_object| iota_object.object_id().unwrap()) + .collect::>(); - object_ids.extend_from_slice(&[ - ObjectID::from_str("0x9a934a2644c4ca2decbe3d126d80720429c5e31896aa756765afa23ae2cb4b99") + object_ids.extend_from_slice(&[ + ObjectID::from_str( + "0x9a934a2644c4ca2decbe3d126d80720429c5e31896aa756765afa23ae2cb4b99", + ) .unwrap(), - ObjectID::from_str("0x1a934a7644c4cf2decbe3d126d80720429c5e30896aa756765afa23af3cb4b82") + ObjectID::from_str( + "0x1a934a7644c4cf2decbe3d126d80720429c5e30896aa756765afa23af3cb4b82", + ) .unwrap(), - ]); + ]); - let indexer_objects = indexer_client - .multi_get_objects(object_ids, None) - .await - .unwrap(); + let indexer_objects = client.multi_get_objects(object_ids, None).await.unwrap(); - let obj_found_num = indexer_objects - .iter() - .filter(|obj_response| obj_response.data.is_some()) - .count(); + let obj_found_num = indexer_objects + .iter() + .filter(|obj_response| obj_response.data.is_some()) + .count(); - assert_eq!(5, obj_found_num); + assert_eq!(5, obj_found_num); - let obj_not_found_num = indexer_objects - .iter() - .filter(|obj_response| obj_response.error.is_some()) - .count(); + let obj_not_found_num = indexer_objects + .iter() + .filter(|obj_response| obj_response.error.is_some()) + .count(); - assert_eq!(2, obj_not_found_num); + assert_eq!(2, obj_not_found_num); + }); } -#[tokio::test] -#[serial] -async fn multi_get_objects_with_bcs_lossless() { - multi_get_objects_with_options(IotaObjectDataOptions::bcs_lossless()).await; +#[test] +fn multi_get_objects_with_bcs_lossless() { + multi_get_objects_with_options(IotaObjectDataOptions::bcs_lossless()); } -#[tokio::test] -#[serial] -async fn multi_get_objects_with_full_content() { - multi_get_objects_with_options(IotaObjectDataOptions::full_content()).await; +#[test] +fn multi_get_objects_with_full_content() { + multi_get_objects_with_options(IotaObjectDataOptions::full_content()); } -#[tokio::test] -#[serial] -async fn multi_get_objects_with_bcs() { - multi_get_objects_with_options(IotaObjectDataOptions::default().with_bcs()).await; +#[test] +fn multi_get_objects_with_bcs() { + multi_get_objects_with_options(IotaObjectDataOptions::default().with_bcs()); } -#[tokio::test] -#[serial] -async fn multi_get_objects_with_content() { - multi_get_objects_with_options(IotaObjectDataOptions::default().with_content()).await; +#[test] +fn multi_get_objects_with_content() { + multi_get_objects_with_options(IotaObjectDataOptions::default().with_content()); } -#[tokio::test] -#[serial] -async fn multi_get_objects_with_display() { - multi_get_objects_with_options(IotaObjectDataOptions::default().with_display()).await; +#[test] +fn multi_get_objects_with_display() { + multi_get_objects_with_options(IotaObjectDataOptions::default().with_display()); } -#[tokio::test] -#[serial] -async fn multi_get_objects_with_owner() { - multi_get_objects_with_options(IotaObjectDataOptions::default().with_owner()).await; +#[test] +fn multi_get_objects_with_owner() { + multi_get_objects_with_options(IotaObjectDataOptions::default().with_owner()); } -#[tokio::test] -#[serial] -async fn multi_get_objects_with_previous_transaction() { - multi_get_objects_with_options(IotaObjectDataOptions::default().with_previous_transaction()) - .await; +#[test] +fn multi_get_objects_with_previous_transaction() { + multi_get_objects_with_options(IotaObjectDataOptions::default().with_previous_transaction()); } -#[tokio::test] -#[serial] -async fn multi_get_objects_with_type() { - multi_get_objects_with_options(IotaObjectDataOptions::default().with_type()).await; +#[test] +fn multi_get_objects_with_type() { + multi_get_objects_with_options(IotaObjectDataOptions::default().with_type()); } -#[tokio::test] -#[serial] -async fn multi_get_objects_with_storage_rebate() { +#[test] +fn multi_get_objects_with_storage_rebate() { multi_get_objects_with_options(IotaObjectDataOptions { show_storage_rebate: true, ..Default::default() - }) - .await; -} - -#[tokio::test] -#[serial] -async fn get_events() { - let (cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; - - let fullnode_checkpoint = cluster - .rpc_client() - .get_checkpoint(CheckpointId::SequenceNumber(0)) - .await - .unwrap(); - - let events = indexer_client - .get_events(*fullnode_checkpoint.transactions.first().unwrap()) - .await - .unwrap(); - - assert!(!events.is_empty()); -} - -#[tokio::test] -#[serial] -async fn get_events_not_found() { - let (_cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; - - let result = indexer_client.get_events(TransactionDigest::ZERO).await; - - assert!(rpc_call_error_msg_matches( - result, - r#"{"code":-32603,"message":"Indexer failed to read PostgresDB with error: `Record not found`"}"#, - )) -} + }); +} + +#[test] +fn get_events() { + let ApiTestSetup { + runtime, + cluster, + store, + client, + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let fullnode_checkpoint = cluster + .rpc_client() + .get_checkpoint(CheckpointId::SequenceNumber(0)) + .await + .unwrap(); -#[tokio::test] -#[serial] -async fn get_transaction_block() { - let (cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; + let events = client + .get_events(*fullnode_checkpoint.transactions.first().unwrap()) + .await + .unwrap(); - let fullnode_checkpoint = cluster - .rpc_client() - .get_checkpoint(CheckpointId::SequenceNumber(0)) - .await - .unwrap(); + assert!(!events.is_empty()); + }); +} + +#[test] +fn get_events_not_found() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let result = client.get_events(TransactionDigest::ZERO).await; + + assert!(rpc_call_error_msg_matches( + result, + r#"{"code":-32603,"message":"Indexer failed to read PostgresDB with error: `Record not found`"}"#, + )) + }); +} + +#[test] +fn get_transaction_block() { + let ApiTestSetup { + runtime, + cluster, + store, + client, + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let fullnode_checkpoint = cluster + .rpc_client() + .get_checkpoint(CheckpointId::SequenceNumber(0)) + .await + .unwrap(); - let tx_digest = *fullnode_checkpoint.transactions.first().unwrap(); + let tx_digest = *fullnode_checkpoint.transactions.first().unwrap(); - let tx = indexer_client - .get_transaction_block(tx_digest, None) - .await - .unwrap(); + let tx = client.get_transaction_block(tx_digest, None).await.unwrap(); - assert_eq!(tx_digest, tx.digest); + assert_eq!(tx_digest, tx.digest); + }); } -#[tokio::test] -#[serial] -async fn get_transaction_block_not_found() { - let (_cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; +#[test] +fn get_transaction_block_not_found() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; - let result = indexer_client - .get_transaction_block(TransactionDigest::ZERO, None) - .await; + let result = client + .get_transaction_block(TransactionDigest::ZERO, None) + .await; - assert!(rpc_call_error_msg_matches( - result, - r#"{"code":-32603,"message":"Invalid argument with error: `Transaction 11111111111111111111111111111111 not found`"}"#, - )); + assert!(rpc_call_error_msg_matches( + result, + r#"{"code":-32603,"message":"Invalid argument with error: `Transaction 11111111111111111111111111111111 not found`"}"#, + )); + }); } -#[tokio::test] -#[serial] -async fn get_transaction_block_with_full_content() { - get_transaction_block_with_options(IotaTransactionBlockResponseOptions::full_content()).await; +#[test] +fn get_transaction_block_with_full_content() { + get_transaction_block_with_options(IotaTransactionBlockResponseOptions::full_content()); } -#[tokio::test] -#[serial] -async fn get_transaction_block_with_full_content_and_with_raw_effects() { +#[test] +fn get_transaction_block_with_full_content_and_with_raw_effects() { get_transaction_block_with_options( IotaTransactionBlockResponseOptions::full_content().with_raw_effects(), - ) - .await; + ); } -#[tokio::test] -#[serial] -async fn get_transaction_block_with_raw_input() { +#[test] +fn get_transaction_block_with_raw_input() { get_transaction_block_with_options( IotaTransactionBlockResponseOptions::default().with_raw_input(), - ) - .await; + ); } -#[tokio::test] -#[serial] -async fn get_transaction_block_with_effects() { +#[test] +fn get_transaction_block_with_effects() { get_transaction_block_with_options( IotaTransactionBlockResponseOptions::default().with_effects(), - ) - .await; + ); } -#[tokio::test] -#[serial] -async fn get_transaction_block_with_events() { +#[test] +fn get_transaction_block_with_events() { get_transaction_block_with_options( IotaTransactionBlockResponseOptions::default().with_events(), - ) - .await; + ); } -#[tokio::test] -#[serial] -async fn get_transaction_block_with_balance_changes() { +#[test] +fn get_transaction_block_with_balance_changes() { get_transaction_block_with_options( IotaTransactionBlockResponseOptions::default().with_balance_changes(), - ) - .await; + ); } -#[tokio::test] -#[serial] -async fn get_transaction_block_with_object_changes() { +#[test] +fn get_transaction_block_with_object_changes() { get_transaction_block_with_options( IotaTransactionBlockResponseOptions::default().with_object_changes(), - ) - .await; + ); } -#[tokio::test] -#[serial] -async fn get_transaction_block_with_raw_effects() { +#[test] +fn get_transaction_block_with_raw_effects() { get_transaction_block_with_options( IotaTransactionBlockResponseOptions::default().with_raw_effects(), - ) - .await; + ); } -#[tokio::test] -#[serial] -async fn get_transaction_block_with_input() { - get_transaction_block_with_options(IotaTransactionBlockResponseOptions::default().with_input()) - .await; +#[test] +fn get_transaction_block_with_input() { + get_transaction_block_with_options(IotaTransactionBlockResponseOptions::default().with_input()); } -#[tokio::test] -#[serial] -async fn multi_get_transaction_blocks() { - let (cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 3).await; +#[test] +fn multi_get_transaction_blocks() { + let ApiTestSetup { + runtime, + cluster, + store, + client, + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 3).await; - let fullnode_checkpoints = cluster - .rpc_client() - .get_checkpoints(None, Some(3), false) - .await - .unwrap(); + let fullnode_checkpoints = cluster + .rpc_client() + .get_checkpoints(None, Some(3), false) + .await + .unwrap(); - let digests = fullnode_checkpoints - .data - .into_iter() - .flat_map(|c| c.transactions) - .collect::>(); + let digests = fullnode_checkpoints + .data + .into_iter() + .flat_map(|c| c.transactions) + .collect::>(); - let fullnode_txs = cluster - .rpc_client() - .multi_get_transaction_blocks(digests.clone(), None) - .await - .unwrap(); + let fullnode_txs = cluster + .rpc_client() + .multi_get_transaction_blocks(digests.clone(), None) + .await + .unwrap(); - let indexer_txs = indexer_client - .multi_get_transaction_blocks(digests, None) - .await - .unwrap(); + let indexer_txs = client + .multi_get_transaction_blocks(digests, None) + .await + .unwrap(); - assert_eq!(fullnode_txs, indexer_txs); + assert_eq!(fullnode_txs, indexer_txs); + }); } -#[tokio::test] -#[serial] -async fn multi_get_transaction_blocks_with_full_content() { - multi_get_transaction_blocks_with_options(IotaTransactionBlockResponseOptions::full_content()) - .await; +#[test] +fn multi_get_transaction_blocks_with_full_content() { + multi_get_transaction_blocks_with_options(IotaTransactionBlockResponseOptions::full_content()); } -#[tokio::test] -#[serial] -async fn multi_get_transaction_blocks_with_full_content_and_with_raw_effects() { +#[test] +fn multi_get_transaction_blocks_with_full_content_and_with_raw_effects() { multi_get_transaction_blocks_with_options( IotaTransactionBlockResponseOptions::full_content().with_raw_effects(), - ) - .await; + ); } -#[tokio::test] -#[serial] -async fn multi_get_transaction_blocks_with_raw_input() { +#[test] +fn multi_get_transaction_blocks_with_raw_input() { multi_get_transaction_blocks_with_options( IotaTransactionBlockResponseOptions::default().with_raw_input(), - ) - .await; + ); } -#[tokio::test] -#[serial] -async fn multi_get_transaction_blocks_with_effects() { +#[test] +fn multi_get_transaction_blocks_with_effects() { multi_get_transaction_blocks_with_options( IotaTransactionBlockResponseOptions::default().with_effects(), - ) - .await; + ); } -#[tokio::test] -#[serial] -async fn multi_get_transaction_blocks_with_events() { +#[test] +fn multi_get_transaction_blocks_with_events() { multi_get_transaction_blocks_with_options( IotaTransactionBlockResponseOptions::default().with_events(), - ) - .await; + ); } -#[tokio::test] -#[serial] -async fn multi_get_transaction_blocks_with_balance_changes() { +#[test] +fn multi_get_transaction_blocks_with_balance_changes() { multi_get_transaction_blocks_with_options( IotaTransactionBlockResponseOptions::default().with_balance_changes(), - ) - .await; + ); } -#[tokio::test] -#[serial] -async fn multi_get_transaction_blocks_with_object_changes() { +#[test] +fn multi_get_transaction_blocks_with_object_changes() { multi_get_transaction_blocks_with_options( IotaTransactionBlockResponseOptions::default().with_object_changes(), - ) - .await; + ); } -#[tokio::test] -#[serial] -async fn multi_get_transaction_blocks_with_raw_effects() { +#[test] +fn multi_get_transaction_blocks_with_raw_effects() { multi_get_transaction_blocks_with_options( IotaTransactionBlockResponseOptions::default().with_raw_effects(), - ) - .await; + ); } -#[tokio::test] -#[serial] -async fn multi_get_transaction_blocks_with_input() { +#[test] +fn multi_get_transaction_blocks_with_input() { multi_get_transaction_blocks_with_options( IotaTransactionBlockResponseOptions::default().with_input(), - ) - .await; -} - -#[tokio::test] -#[serial] -async fn get_protocol_config() { - let (cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; - - let fullnode_protocol_config = cluster - .rpc_client() - .get_protocol_config(None) - .await - .unwrap(); - - let indexer_protocol_config = indexer_client.get_protocol_config(None).await.unwrap(); - - assert_eq!(fullnode_protocol_config, indexer_protocol_config); - - let indexer_protocol_config = indexer_client - .get_protocol_config(Some(1u64.into())) - .await - .unwrap(); - - assert_eq!(fullnode_protocol_config, indexer_protocol_config); -} - -#[tokio::test] -#[serial] -async fn get_protocol_config_invalid_protocol_version() { - let (_cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; - - let result = indexer_client - .get_protocol_config(Some(100u64.into())) - .await; - - assert!(rpc_call_error_msg_matches( - result, - r#"{"code":-32603,"message":"Unsupported protocol version requested. Min supported: 1, max supported: 1"}"#, - )); -} - -#[tokio::test] -#[serial] -async fn get_chain_identifier() { - let (cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; - - let fullnode_chain_identifier = cluster.rpc_client().get_chain_identifier().await.unwrap(); - - let indexer_chain_identifier = indexer_client.get_chain_identifier().await.unwrap(); - - assert_eq!(fullnode_chain_identifier, indexer_chain_identifier) -} - -#[tokio::test] -#[serial] -async fn get_total_transaction_blocks() { - let stop_after_checkpoint_seq = 5; - let (cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(Some(stop_after_checkpoint_seq)).await; - - let run_with_range = cluster - .wait_for_run_with_range_shutdown_signal() - .await - .unwrap(); - - assert!(matches!( - run_with_range, - RunWithRange::Checkpoint(checkpoint_seq_num) if checkpoint_seq_num == stop_after_checkpoint_seq - )); - - // ensure the highest synced checkpoint matches - assert!(cluster.fullnode_handle.iota_node.with(|node| { - node.state() - .get_checkpoint_store() - .get_highest_executed_checkpoint_seq_number() - .unwrap() - == Some(stop_after_checkpoint_seq) - })); - - let checkpoint = cluster - .fullnode_handle - .iota_node - .with(|node| { - node.state() - .get_checkpoint_store() - .get_checkpoint_by_sequence_number(stop_after_checkpoint_seq) - .unwrap() - }) - .unwrap(); + ); +} - indexer_wait_for_checkpoint(&pg_store, stop_after_checkpoint_seq).await; +#[test] +fn get_protocol_config() { + let ApiTestSetup { + runtime, + cluster, + store, + client, + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let fullnode_protocol_config = cluster + .rpc_client() + .get_protocol_config(None) + .await + .unwrap(); - let total_transaction_blocks = indexer_client - .get_total_transaction_blocks() - .await - .unwrap() - .into_inner(); + let indexer_protocol_config = client.get_protocol_config(None).await.unwrap(); - assert_eq!( - checkpoint.network_total_transactions, - total_transaction_blocks - ); + assert_eq!(fullnode_protocol_config, indexer_protocol_config); + + let indexer_protocol_config = client.get_protocol_config(Some(1u64.into())).await.unwrap(); + + assert_eq!(fullnode_protocol_config, indexer_protocol_config); + }); } -#[tokio::test] -#[serial] -async fn get_latest_checkpoint_sequence_number() { - let stop_after_checkpoint_seq = 5; - let (cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(Some(stop_after_checkpoint_seq)).await; - - let run_with_range = cluster - .wait_for_run_with_range_shutdown_signal() - .await - .unwrap(); - - assert!(matches!( - run_with_range, - RunWithRange::Checkpoint(checkpoint_seq_num) if checkpoint_seq_num == stop_after_checkpoint_seq - )); - - // ensure the highest synced checkpoint matches - assert!(cluster.fullnode_handle.iota_node.with(|node| { - node.state() - .get_checkpoint_store() - .get_highest_executed_checkpoint_seq_number() - .unwrap() - == Some(stop_after_checkpoint_seq) - })); - - let fullnode_latest_checkpoint_seq_number = cluster - .rpc_client() - .get_latest_checkpoint_sequence_number() - .await - .unwrap() - .into_inner(); - - indexer_wait_for_checkpoint(&pg_store, stop_after_checkpoint_seq).await; - - let latest_checkpoint_seq_number = indexer_client - .get_latest_checkpoint_sequence_number() - .await - .unwrap() - .into_inner(); - - assert!( - (stop_after_checkpoint_seq == latest_checkpoint_seq_number) - && (stop_after_checkpoint_seq == fullnode_latest_checkpoint_seq_number) - ); +#[test] +fn get_protocol_config_invalid_protocol_version() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let result = client + .get_protocol_config(Some(100u64.into())) + .await; + + assert!(rpc_call_error_msg_matches( + result, + r#"{"code":-32603,"message":"Unsupported protocol version requested. Min supported: 1, max supported: 1"}"#, + )); + }); } -#[tokio::test] -#[serial] -async fn try_get_past_object() { - let (_cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; - - let result = indexer_client - .try_get_past_object(ObjectID::random(), SequenceNumber::new(), None) - .await; - assert!(rpc_call_error_msg_matches( - result, - r#"{"code":-32601,"message":"Method not found"}"# - )); -} - -#[tokio::test] -#[serial] -async fn try_multi_get_past_objects() { - let (_cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; - - let result = indexer_client - .try_multi_get_past_objects( - vec![IotaGetPastObjectRequest { - object_id: ObjectID::random(), - version: SequenceNumber::new(), - }], - None, - ) - .await; - assert!(rpc_call_error_msg_matches( - result, - r#"{"code":-32601,"message":"Method not found"}"# - )); -} - -#[tokio::test] -#[serial] -async fn get_loaded_child_objects() { - let (_cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; - - let result = indexer_client - .get_loaded_child_objects(TransactionDigest::ZERO) - .await; - assert!(rpc_call_error_msg_matches( - result, - r#"{"code":-32601,"message":"Method not found"}"# - )); +#[test] +fn get_chain_identifier() { + let ApiTestSetup { + runtime, + cluster, + store, + client, + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let fullnode_chain_identifier = cluster.rpc_client().get_chain_identifier().await.unwrap(); + + let indexer_chain_identifier = client.get_chain_identifier().await.unwrap(); + + assert_eq!(fullnode_chain_identifier, indexer_chain_identifier) + }); +} + +#[test] +fn get_total_transaction_blocks() { + let checkpoint = 5; + let ApiTestSetup { + runtime, + cluster, + store, + client, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, checkpoint).await; + let total_transaction_blocks = client.get_total_transaction_blocks().await.unwrap(); + + let fullnode_checkpoint = cluster + .rpc_client() + .get_checkpoint(CheckpointId::SequenceNumber(checkpoint)) + .await + .unwrap(); + + let indexer_checkpoint = client + .get_checkpoint(CheckpointId::SequenceNumber(checkpoint)) + .await + .unwrap(); + + assert!( + total_transaction_blocks.into_inner() >= fullnode_checkpoint.network_total_transactions + ); + assert!( + total_transaction_blocks.into_inner() >= indexer_checkpoint.network_total_transactions, + ); + }); +} + +#[test] +fn get_latest_checkpoint_sequence_number() { + let checkpoint = 5; + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, checkpoint).await; + + let latest_checkpoint_seq_number = client + .get_latest_checkpoint_sequence_number() + .await + .unwrap(); + + assert!(latest_checkpoint_seq_number.into_inner() >= checkpoint); + + indexer_wait_for_checkpoint(store, checkpoint + 5).await; + + let latest_checkpoint_seq_number = client + .get_latest_checkpoint_sequence_number() + .await + .unwrap(); + + assert!(latest_checkpoint_seq_number.into_inner() >= checkpoint + 5); + }); +} + +#[test] +fn try_get_past_object() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let result = client + .try_get_past_object(ObjectID::random(), SequenceNumber::new(), None) + .await; + assert!(rpc_call_error_msg_matches( + result, + r#"{"code":-32601,"message":"Method not found"}"# + )); + }); +} + +#[test] +fn try_multi_get_past_objects() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let result = client + .try_multi_get_past_objects( + vec![IotaGetPastObjectRequest { + object_id: ObjectID::random(), + version: SequenceNumber::new(), + }], + None, + ) + .await; + assert!(rpc_call_error_msg_matches( + result, + r#"{"code":-32601,"message":"Method not found"}"# + )); + }); +} + +#[test] +fn get_loaded_child_objects() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let result = client + .get_loaded_child_objects(TransactionDigest::ZERO) + .await; + assert!(rpc_call_error_msg_matches( + result, + r#"{"code":-32601,"message":"Method not found"}"# + )); + }); } From a84eda737422fbb326216e09fd40271aa55e9545 Mon Sep 17 00:00:00 2001 From: Sergiu Popescu <44298302+sergiupopescu199@users.noreply.github.com> Date: Tue, 1 Oct 2024 11:12:45 +0200 Subject: [PATCH 05/24] fix(iota-indexer): Refactor `IndexerApi` top use the shared runtime for tests (#2964) * fix(iota-indexer): refactor indexer_api to use shared runtime in tests * fixup! fix(iota-indexer): refactor indexer_api to use shared runtime in tests --- .../tests/rpc-tests/indexer_api.rs | 272 ++++++++++-------- crates/iota-indexer/tests/rpc-tests/main.rs | 2 +- 2 files changed, 146 insertions(+), 128 deletions(-) diff --git a/crates/iota-indexer/tests/rpc-tests/indexer_api.rs b/crates/iota-indexer/tests/rpc-tests/indexer_api.rs index 1f93b04d472..5acaf25ea64 100644 --- a/crates/iota-indexer/tests/rpc-tests/indexer_api.rs +++ b/crates/iota-indexer/tests/rpc-tests/indexer_api.rs @@ -9,138 +9,156 @@ use iota_types::{ base_types::{IotaAddress, ObjectID}, digests::TransactionDigest, }; -use serial_test::serial; -use crate::common::{ - indexer_wait_for_checkpoint, rpc_call_error_msg_matches, - start_test_cluster_with_read_write_indexer, -}; - -#[tokio::test] -#[serial] -async fn query_events_no_events_descending() { - let (_cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; - - let indexer_events = indexer_client - .query_events( - EventFilter::Sender( - IotaAddress::from_str( - "0x9a934a2644c4ca2decbe3d126d80720429c5e31896aa756765afa23ae2cb4b99", - ) - .unwrap(), - ), - None, - None, - Some(true), - ) - .await - .unwrap(); - - assert_eq!(indexer_events, EventPage::empty()) +use crate::common::{indexer_wait_for_checkpoint, rpc_call_error_msg_matches, ApiTestSetup}; + +#[test] +fn query_events_no_events_descending() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let indexer_events = client + .query_events( + EventFilter::Sender( + IotaAddress::from_str( + "0x9a934a2644c4ca2decbe3d126d80720429c5e31896aa756765afa23ae2cb4b99", + ) + .unwrap(), + ), + None, + None, + Some(true), + ) + .await + .unwrap(); + + assert_eq!(indexer_events, EventPage::empty()) + }); } -#[tokio::test] -#[serial] -async fn query_events_no_events_ascending() { - let (_cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; - - let indexer_events = indexer_client - .query_events( - EventFilter::Sender( - IotaAddress::from_str( - "0x9a934a2644c4ca2decbe3d126d80720429c5e31896aa756765afa23ae2cb4b99", - ) - .unwrap(), - ), - None, - None, - None, - ) - .await - .unwrap(); - - assert_eq!(indexer_events, EventPage::empty()) +#[test] +fn query_events_no_events_ascending() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let indexer_events = client + .query_events( + EventFilter::Sender( + IotaAddress::from_str( + "0x9a934a2644c4ca2decbe3d126d80720429c5e31896aa756765afa23ae2cb4b99", + ) + .unwrap(), + ), + None, + None, + None, + ) + .await + .unwrap(); + + assert_eq!(indexer_events, EventPage::empty()) + }); } -#[tokio::test] -#[serial] -async fn query_events_unsupported_events() { - let (_cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; - - // Get the current time in milliseconds since the UNIX epoch - let now_millis = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_millis(); - - // Subtract 10 minutes from the current time - let ten_minutes_ago = now_millis - (10 * 60 * 1000); // 600 seconds = 10 minutes - - let unsupported_filters = vec![ - EventFilter::All(vec![]), - EventFilter::Any(vec![]), - EventFilter::And( - Box::new(EventFilter::Any(vec![])), - Box::new(EventFilter::Any(vec![])), - ), - EventFilter::Or( - Box::new(EventFilter::Any(vec![])), - Box::new(EventFilter::Any(vec![])), - ), - EventFilter::TimeRange { - start_time: ten_minutes_ago as u64, - end_time: now_millis as u64, - }, - EventFilter::MoveEventField { - path: String::default(), - value: serde_json::Value::Bool(true), - }, - ]; - - for event_filter in unsupported_filters { - let result = indexer_client - .query_events(event_filter, None, None, None) - .await; - - assert!(rpc_call_error_msg_matches( - result, - r#"{"code":-32603,"message": "Indexer does not support the feature with error: `This type of EventFilter is not supported.`"}"#, - )); - } +#[test] +fn query_events_unsupported_events() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + // Get the current time in milliseconds since the UNIX epoch + let now_millis = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_millis(); + + // Subtract 10 minutes from the current time + let ten_minutes_ago = now_millis - (10 * 60 * 1000); // 600 seconds = 10 minutes + + let unsupported_filters = vec![ + EventFilter::All(vec![]), + EventFilter::Any(vec![]), + EventFilter::And( + Box::new(EventFilter::Any(vec![])), + Box::new(EventFilter::Any(vec![])), + ), + EventFilter::Or( + Box::new(EventFilter::Any(vec![])), + Box::new(EventFilter::Any(vec![])), + ), + EventFilter::TimeRange { + start_time: ten_minutes_ago as u64, + end_time: now_millis as u64, + }, + EventFilter::MoveEventField { + path: String::default(), + value: serde_json::Value::Bool(true), + }, + ]; + + for event_filter in unsupported_filters { + let result = client + .query_events(event_filter, None, None, None) + .await; + + assert!(rpc_call_error_msg_matches( + result, + r#"{"code":-32603,"message": "Indexer does not support the feature with error: `This type of EventFilter is not supported.`"}"#, + )); + } + }); } -#[tokio::test] -#[serial] -async fn query_events_supported_events() { - let (_cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 1).await; - - let supported_filters = vec![ - EventFilter::Sender(IotaAddress::ZERO), - EventFilter::Transaction(TransactionDigest::ZERO), - EventFilter::Package(ObjectID::ZERO), - EventFilter::MoveEventModule { - package: ObjectID::ZERO, - module: "x".parse().unwrap(), - }, - EventFilter::MoveEventType("0xabcd::MyModule::Foo".parse().unwrap()), - EventFilter::MoveModule { - package: ObjectID::ZERO, - module: "x".parse().unwrap(), - }, - ]; - - for event_filter in supported_filters { - let result = indexer_client - .query_events(event_filter, None, None, None) - .await; - assert!(result.is_ok()); - } +#[test] +fn query_events_supported_events() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let supported_filters = vec![ + EventFilter::Sender(IotaAddress::ZERO), + EventFilter::Transaction(TransactionDigest::ZERO), + EventFilter::Package(ObjectID::ZERO), + EventFilter::MoveEventModule { + package: ObjectID::ZERO, + module: "x".parse().unwrap(), + }, + EventFilter::MoveEventType("0xabcd::MyModule::Foo".parse().unwrap()), + EventFilter::MoveModule { + package: ObjectID::ZERO, + module: "x".parse().unwrap(), + }, + ]; + + for event_filter in supported_filters { + let result = client.query_events(event_filter, None, None, None).await; + assert!(result.is_ok()); + } + }); } diff --git a/crates/iota-indexer/tests/rpc-tests/main.rs b/crates/iota-indexer/tests/rpc-tests/main.rs index df7236bdbda..10826b1da04 100644 --- a/crates/iota-indexer/tests/rpc-tests/main.rs +++ b/crates/iota-indexer/tests/rpc-tests/main.rs @@ -8,7 +8,7 @@ mod common; #[cfg(feature = "pg_integration")] mod extended_api; -#[cfg(feature = "pg_integration")] +#[cfg(feature = "shared_test_runtime")] mod indexer_api; #[cfg(feature = "shared_test_runtime")] From 4e29487b78a03a67a5202b1b356e068d879ee707 Mon Sep 17 00:00:00 2001 From: Sergiu Popescu <44298302+sergiupopescu199@users.noreply.github.com> Date: Wed, 23 Oct 2024 15:12:54 +0200 Subject: [PATCH 06/24] feat(iota-indexer): Add `WriteApi` tests (#3488) * feat(iota-indexer): add write_api tests * fixup! feat(iota-indexer): add write_api tests * fixup! fixup! feat(iota-indexer): add write_api tests * fixup! fixup! fixup! feat(iota-indexer): add write_api tests * fixup! fixup! fixup! fixup! feat(iota-indexer): add write_api tests --- crates/iota-indexer/Cargo.toml | 1 + crates/iota-indexer/tests/common/mod.rs | 35 ++- crates/iota-indexer/tests/rpc-tests/main.rs | 3 + .../iota-indexer/tests/rpc-tests/write_api.rs | 248 ++++++++++++++++++ 4 files changed, 283 insertions(+), 4 deletions(-) create mode 100644 crates/iota-indexer/tests/rpc-tests/write_api.rs diff --git a/crates/iota-indexer/Cargo.toml b/crates/iota-indexer/Cargo.toml index 2d58d37510d..54ac2861ba7 100644 --- a/crates/iota-indexer/Cargo.toml +++ b/crates/iota-indexer/Cargo.toml @@ -53,6 +53,7 @@ pg_integration = [] shared_test_runtime = [] [dev-dependencies] +fastcrypto.workspace = true iota-config.workspace = true iota-genesis-builder.workspace = true iota-keys.workspace = true diff --git a/crates/iota-indexer/tests/common/mod.rs b/crates/iota-indexer/tests/common/mod.rs index c3ab57b8634..3c4697c722a 100644 --- a/crates/iota-indexer/tests/common/mod.rs +++ b/crates/iota-indexer/tests/common/mod.rs @@ -9,14 +9,17 @@ use std::{ use iota_config::node::RunWithRange; use iota_indexer::{ + IndexerConfig, errors::IndexerError, indexer::Indexer, - store::{indexer_store::IndexerStore, PgIndexerStore}, - test_utils::{start_test_indexer, ReaderWriterConfig}, - IndexerConfig, + store::{PgIndexerStore, indexer_store::IndexerStore}, + test_utils::{ReaderWriterConfig, start_test_indexer}, }; use iota_metrics::init_metrics; -use iota_types::storage::ReadStore; +use iota_types::{ + base_types::{ObjectID, SequenceNumber}, + storage::ReadStore, +}; use jsonrpsee::{ http_client::{HttpClient, HttpClientBuilder}, types::ErrorObject, @@ -117,6 +120,30 @@ pub async fn indexer_wait_for_checkpoint( .expect("Timeout waiting for indexer to catchup to checkpoint"); } +/// Wait for the indexer to catch up to the given object sequence number +pub async fn indexer_wait_for_object( + pg_store: &PgIndexerStore, + object_id: ObjectID, + sequence_number: SequenceNumber, +) { + tokio::time::timeout(Duration::from_secs(30), async { + loop { + if pg_store + .get_object_read(object_id, Some(sequence_number)) + .await + .unwrap() + .object() + .is_ok() + { + break; + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + }) + .await + .expect("Timeout waiting for indexer to catchup to given object's sequence number"); +} + /// Start an Indexer instance in `Read` mode fn start_indexer_reader(fullnode_rpc_url: impl Into) { let config = IndexerConfig { diff --git a/crates/iota-indexer/tests/rpc-tests/main.rs b/crates/iota-indexer/tests/rpc-tests/main.rs index 10826b1da04..27a2c9f3e62 100644 --- a/crates/iota-indexer/tests/rpc-tests/main.rs +++ b/crates/iota-indexer/tests/rpc-tests/main.rs @@ -13,3 +13,6 @@ mod indexer_api; #[cfg(feature = "shared_test_runtime")] mod read_api; + +#[cfg(feature = "shared_test_runtime")] +mod write_api; diff --git a/crates/iota-indexer/tests/rpc-tests/write_api.rs b/crates/iota-indexer/tests/rpc-tests/write_api.rs new file mode 100644 index 00000000000..304559c9c4b --- /dev/null +++ b/crates/iota-indexer/tests/rpc-tests/write_api.rs @@ -0,0 +1,248 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use fastcrypto::encoding::Base64; +use iota_indexer::store::indexer_store::IndexerStore; +use iota_json_rpc_api::{ + IndexerApiClient, ReadApiClient, TransactionBuilderClient, WriteApiClient, +}; +use iota_json_rpc_types::{ + IotaExecutionStatus, IotaObjectDataOptions, IotaTransactionBlockEffectsAPI, + IotaTransactionBlockResponseOptions, +}; +use iota_types::{ + base_types::{IotaAddress, ObjectID}, + object::Owner, + programmable_transaction_builder::ProgrammableTransactionBuilder, + quorum_driver_types::ExecuteTransactionRequestType, + transaction::TransactionKind, +}; +use jsonrpsee::http_client::HttpClient; +use test_cluster::TestCluster; + +use crate::common::{ApiTestSetup, indexer_wait_for_checkpoint, indexer_wait_for_object}; + +type TxBytes = Base64; +type Signatures = Vec; +async fn prepare_and_sign_to_transfer_first_object( + sender: IotaAddress, + receiver: IotaAddress, + cluster: &TestCluster, + client: &HttpClient, +) -> (ObjectID, TxBytes, Signatures) { + let objects = cluster + .rpc_client() + .get_owned_objects(sender, None, None, None) + .await + .unwrap() + .data; + + let obj_id = objects.first().unwrap().object().unwrap().object_id; + let gas = objects.last().unwrap().object().unwrap().object_id; + + let transaction_bytes = client + .transfer_object(sender, obj_id, Some(gas), 10_000_000.into(), receiver) + .await + .unwrap(); + + let (tx_bytes, signatures) = cluster + .wallet + .sign_transaction(&transaction_bytes.to_data().unwrap()) + .to_tx_bytes_and_signatures(); + + (obj_id, tx_bytes, signatures) +} + +#[test] +fn dev_inspect_transaction_block() { + let ApiTestSetup { + runtime, + cluster, + store, + client, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async { + indexer_wait_for_checkpoint(store, 1).await; + + let sender = cluster.get_address_0(); + let receiver = cluster.get_address_1(); + + let objects = cluster + .rpc_client() + .get_owned_objects(sender, None, None, None) + .await + .unwrap() + .data; + + let (obj_id, seq_num, digest) = objects.first().unwrap().object().unwrap().object_ref(); + + let mut builder = ProgrammableTransactionBuilder::new(); + builder + .transfer_object(receiver, (obj_id, seq_num, digest)) + .unwrap(); + let ptb = builder.finish(); + + let indexer_devinspect_results = client + .dev_inspect_transaction_block( + sender, + Base64::from_bytes(&bcs::to_bytes(&TransactionKind::programmable(ptb)).unwrap()), + None, + None, + None, + ) + .await + .unwrap(); + + assert_eq!( + *indexer_devinspect_results.effects.status(), + IotaExecutionStatus::Success + ); + + let (new_seq_num, owner) = indexer_devinspect_results + .effects + .mutated() + .iter() + .find_map(|obj| { + (obj.reference.object_id == obj_id).then_some((obj.reference.version, obj.owner)) + }) + .unwrap(); + + assert_eq!(owner, Owner::AddressOwner(receiver)); + + let latest_checkpoint_seq_number = client + .get_latest_checkpoint_sequence_number() + .await + .unwrap(); + + indexer_wait_for_checkpoint(store, latest_checkpoint_seq_number.into_inner() + 1).await; + assert!( + store + .get_object_read(obj_id, Some(new_seq_num)) + .await + .unwrap() + .object() + .is_err(), + "The actual object should not have the sequence number incremented" + ); + + let actual_object_info = client + .get_object(obj_id, Some(IotaObjectDataOptions::new().with_owner())) + .await + .unwrap(); + + assert_eq!( + actual_object_info.data.unwrap().owner.unwrap(), + Owner::AddressOwner(sender), + "The initial owner of the object should not change" + ); + }); +} + +#[test] +fn execute_transaction_block() { + let ApiTestSetup { + runtime, + cluster, + store, + client, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async { + indexer_wait_for_checkpoint(store, 1).await; + + let sender = cluster.get_address_0(); + let receiver = cluster.get_address_1(); + + let (obj_id, tx_bytes, signatures) = + prepare_and_sign_to_transfer_first_object(sender, receiver, cluster, client).await; + + let indexer_tx_response = client + .execute_transaction_block( + tx_bytes, + signatures, + Some(IotaTransactionBlockResponseOptions::new().with_effects()), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await + .unwrap(); + + assert_eq!( + *indexer_tx_response.effects.as_ref().unwrap().status(), + IotaExecutionStatus::Success + ); + + let (seq_num, owner) = indexer_tx_response + .effects + .unwrap() + .mutated() + .iter() + .find_map(|obj| { + (obj.reference.object_id == obj_id).then_some((obj.reference.version, obj.owner)) + }) + .unwrap(); + + assert_eq!(owner, Owner::AddressOwner(receiver)); + + indexer_wait_for_object(store, obj_id, seq_num).await; + + let actual_object_info = client + .get_object(obj_id, Some(IotaObjectDataOptions::new().with_owner())) + .await + .unwrap(); + + assert_eq!( + actual_object_info.data.unwrap().owner.unwrap(), + Owner::AddressOwner(receiver) + ); + }); +} + +#[test] +fn dry_run_transaction_block() { + let ApiTestSetup { + runtime, + cluster, + store, + client, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async { + indexer_wait_for_checkpoint(store, 1).await; + + let sender = cluster.get_address_0(); + let receiver = cluster.get_address_1(); + + let (_, tx_bytes, signatures) = + prepare_and_sign_to_transfer_first_object(sender, receiver, cluster, client).await; + + let dry_run_tx_block_resp = client + .dry_run_transaction_block(tx_bytes.clone()) + .await + .unwrap(); + + let indexer_tx_response = client + .execute_transaction_block( + tx_bytes, + signatures, + Some( + IotaTransactionBlockResponseOptions::new() + .with_effects() + .with_object_changes(), + ), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await + .unwrap(); + + assert_eq!( + *indexer_tx_response.effects.as_ref().unwrap().status(), + IotaExecutionStatus::Success + ); + + assert_eq!( + indexer_tx_response.object_changes.unwrap(), + dry_run_tx_block_resp.object_changes + ) + }); +} From 5b1c65bd8892dc7f0eaed8e4c892e4db1900dfda Mon Sep 17 00:00:00 2001 From: Sergiu Popescu <44298302+sergiupopescu199@users.noreply.github.com> Date: Mon, 28 Oct 2024 11:01:13 +0100 Subject: [PATCH 07/24] feat(iota-indexer): add move_utils tests (#3599) * feat(iota-indexer): add move_utils tests * fixup! Merge branch 'sc-platform/indexer-new-rpc-tests' into sc-platform/issue-2200 * fixup! fixup! Merge branch 'sc-platform/indexer-new-rpc-tests' into sc-platform/issue-2200 * fixup! fixup! fixup! Merge branch 'sc-platform/indexer-new-rpc-tests' into sc-platform/issue-2200 * fix(iota-indexer): fix move_utils tests --- crates/iota-indexer/tests/rpc-tests/main.rs | 3 + .../tests/rpc-tests/move_utils.rs | 331 ++++++++++++++++++ crates/iota-json-rpc-types/src/iota_move.rs | 20 +- 3 files changed, 344 insertions(+), 10 deletions(-) create mode 100644 crates/iota-indexer/tests/rpc-tests/move_utils.rs diff --git a/crates/iota-indexer/tests/rpc-tests/main.rs b/crates/iota-indexer/tests/rpc-tests/main.rs index 27a2c9f3e62..ccf62fbf0a6 100644 --- a/crates/iota-indexer/tests/rpc-tests/main.rs +++ b/crates/iota-indexer/tests/rpc-tests/main.rs @@ -11,6 +11,9 @@ mod extended_api; #[cfg(feature = "shared_test_runtime")] mod indexer_api; +#[cfg(feature = "shared_test_runtime")] +mod move_utils; + #[cfg(feature = "shared_test_runtime")] mod read_api; diff --git a/crates/iota-indexer/tests/rpc-tests/move_utils.rs b/crates/iota-indexer/tests/rpc-tests/move_utils.rs new file mode 100644 index 00000000000..0c4591c0038 --- /dev/null +++ b/crates/iota-indexer/tests/rpc-tests/move_utils.rs @@ -0,0 +1,331 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use iota_json_rpc_api::MoveUtilsClient; +use iota_json_rpc_types::{MoveFunctionArgType, ObjectValueKind}; + +use crate::common::{indexer_wait_for_checkpoint, rpc_call_error_msg_matches, ApiTestSetup}; + +#[test] +fn get_move_function_arg_types_empty() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let indexer_function_args_type = client + .get_move_function_arg_types( + "0x1".parse().unwrap(), + "address".to_owned(), + "length".to_owned(), + ) + .await + .unwrap(); + + assert!( + indexer_function_args_type.is_empty(), + "Should not have any function args" + ) + }); +} + +#[test] +fn get_move_function_arg_types() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let indexer_function_args_type = client + .get_move_function_arg_types( + "0x1".parse().unwrap(), + "vector".to_owned(), + "push_back".to_owned(), + ) + .await + .unwrap(); + + assert!(matches!(indexer_function_args_type.as_slice(), [ + MoveFunctionArgType::Object(ObjectValueKind::ByMutableReference), + MoveFunctionArgType::Pure + ])); + }); +} + +#[test] +fn get_move_function_arg_types_not_found() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let result = client + .get_move_function_arg_types( + "0x1823746".parse().unwrap(), + "vector".to_owned(), + "push_back".to_owned(), + ) + .await; + + assert!(matches!(result, Err(err) if err.to_string().contains("Package not found in DB: 0000000000000000000000000000000000000000000000000000000001823746"))); + + let result = client + .get_move_function_arg_types( + "0x1".parse().unwrap(), + "wrong_module".to_owned(), + "push_back".to_owned(), + ) + .await; + + assert!(rpc_call_error_msg_matches( + result, + r#"{"code":-32602,"message":"No module was found with name wrong_module"}"# + )); + + let result = client + .get_move_function_arg_types( + "0x1".parse().unwrap(), + "vector".to_owned(), + "wrong_function".to_owned(), + ) + .await; + + assert!(rpc_call_error_msg_matches( + result, + r#"{"code":-32602,"message":"No function was found with function name wrong_function"}"# + )); + }); +} + +#[test] +fn get_normalized_move_modules_by_package() { + let ApiTestSetup { + cluster, + runtime, + store, + client, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let package_id = "0x1".parse().unwrap(); + + let fullnode_response = cluster + .rpc_client() + .get_normalized_move_modules_by_package(package_id) + .await + .unwrap(); + + let indexer_response = client + .get_normalized_move_modules_by_package(package_id) + .await + .unwrap(); + + assert_eq!(fullnode_response, indexer_response); + }); +} + +#[test] +fn get_normalized_move_modules_by_package_not_found() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let result = client + .get_normalized_move_modules_by_package("0x1823746".parse().unwrap()) + .await; + + assert!(matches!(result, Err(err) if err.to_string().contains("Package not found in DB: 0000000000000000000000000000000000000000000000000000000001823746"))); + }); +} + +#[test] +fn get_normalized_move_module() { + let ApiTestSetup { + cluster, + runtime, + store, + client, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let package_id = "0x1".parse().unwrap(); + let module = "vector".to_owned(); + + let fullnode_response = cluster + .rpc_client() + .get_normalized_move_module(package_id, module.clone()) + .await + .unwrap(); + + let indexer_response = client + .get_normalized_move_module(package_id, module) + .await + .unwrap(); + + assert_eq!(fullnode_response, indexer_response); + }); +} + +#[test] +fn get_normalized_move_module_not_found() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let result = client + .get_normalized_move_module("0x1".parse().unwrap(), "wrong_module".to_owned()) + .await; + + assert!(rpc_call_error_msg_matches( + result, + r#"{"code":-32602,"message":"No module was found with name wrong_module"}"# + )); + }); +} + +#[test] +fn get_normalized_move_struct() { + let ApiTestSetup { + cluster, + runtime, + store, + client, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let package_id = "0x2".parse().unwrap(); + let module = "vec_set".to_owned(); + let struct_name = "VecSet".to_owned(); + + let fullnode_response = cluster + .rpc_client() + .get_normalized_move_struct(package_id, module.clone(), struct_name.clone()) + .await + .unwrap(); + + let indexer_response = client + .get_normalized_move_struct(package_id, module, struct_name) + .await + .unwrap(); + + assert_eq!(fullnode_response, indexer_response) + }); +} + +#[test] +fn get_normalized_move_struct_not_found() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let result = client + .get_normalized_move_struct( + "0x2".parse().unwrap(), + "vec_set".to_owned(), + "WrongStruct".to_owned(), + ) + .await; + + assert!(rpc_call_error_msg_matches( + result, + r#"{"code":-32602,"message":"No struct was found with struct name WrongStruct"}"# + )); + }); +} + +#[test] +fn get_normalized_move_function() { + let ApiTestSetup { + cluster, + runtime, + store, + client, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let package_id = "0x2".parse().unwrap(); + let module = "vec_set".to_owned(); + let function_name = "insert".to_owned(); + + let fullnode_response = cluster + .rpc_client() + .get_normalized_move_function(package_id, module.clone(), function_name.clone()) + .await + .unwrap(); + + let indexer_response = client + .get_normalized_move_function(package_id, module, function_name) + .await + .unwrap(); + + assert_eq!(fullnode_response, indexer_response) + }); +} + +#[test] +fn get_normalized_move_function_not_found() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let result = client + .get_normalized_move_function( + "0x2".parse().unwrap(), + "vec_set".to_owned(), + "wrong_function".to_owned(), + ) + .await; + + assert!(rpc_call_error_msg_matches( + result, + r#"{"code":-32602,"message":"No function was found with function name wrong_function"}"# + )); + }); +} diff --git a/crates/iota-json-rpc-types/src/iota_move.rs b/crates/iota-json-rpc-types/src/iota_move.rs index 616272a848a..239afb24a09 100644 --- a/crates/iota-json-rpc-types/src/iota_move.rs +++ b/crates/iota-json-rpc-types/src/iota_move.rs @@ -39,7 +39,7 @@ pub type IotaMoveTypeParameterIndex = u16; #[path = "unit_tests/iota_move_tests.rs"] mod iota_move_tests; -#[derive(Serialize, Deserialize, Copy, Clone, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Copy, Clone, Debug, JsonSchema, PartialEq)] pub enum IotaMoveAbility { Copy, Drop, @@ -47,33 +47,33 @@ pub enum IotaMoveAbility { Key, } -#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq)] pub struct IotaMoveAbilitySet { pub abilities: Vec, } -#[derive(Serialize, Deserialize, Copy, Clone, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Copy, Clone, Debug, JsonSchema, PartialEq)] pub enum IotaMoveVisibility { Private, Public, Friend, } -#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq)] #[serde(rename_all = "camelCase")] pub struct IotaMoveStructTypeParameter { pub constraints: IotaMoveAbilitySet, pub is_phantom: bool, } -#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq)] pub struct IotaMoveNormalizedField { pub name: String, #[serde(rename = "type")] pub type_: IotaMoveNormalizedType, } -#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq)] #[serde(rename_all = "camelCase")] pub struct IotaMoveNormalizedStruct { pub abilities: IotaMoveAbilitySet, @@ -81,7 +81,7 @@ pub struct IotaMoveNormalizedStruct { pub fields: Vec, } -#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq)] pub enum IotaMoveNormalizedType { Bool, U8, @@ -105,7 +105,7 @@ pub enum IotaMoveNormalizedType { MutableReference(Box), } -#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq)] #[serde(rename_all = "camelCase")] pub struct IotaMoveNormalizedFunction { pub visibility: IotaMoveVisibility, @@ -292,14 +292,14 @@ impl From for IotaMoveAbilitySet { } } -#[derive(Serialize, Deserialize, Copy, Clone, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Copy, Clone, Debug, JsonSchema, PartialEq)] pub enum ObjectValueKind { ByImmutableReference, ByMutableReference, ByValue, } -#[derive(Serialize, Deserialize, Copy, Clone, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Copy, Clone, Debug, JsonSchema, PartialEq)] pub enum MoveFunctionArgType { Pure, Object(ObjectValueKind), From 16a101776ef3ff6558e6a75d4f58fe882ab66f8c Mon Sep 17 00:00:00 2001 From: Samuel Rufinatscha Date: Mon, 28 Oct 2024 17:05:12 +0100 Subject: [PATCH 08/24] test(iota-indexer): Add tests for `IndexerApi` (#3600) * fix: add indexer indexer-api tests * refactor: simplify Faucet type * refactor: Add dynamic field tests * refactor: pass gas object * refactor: add iota-test-transaction-builder to dev-deps * refactor: fix merge conflicts * refactor: fix fmt * refactor: remove dev dep * refactor: fix fmt * refactor: fix clippy * refactor: remove refs --- Cargo.lock | 1 + crates/iota-indexer/Cargo.toml | 5 +- crates/iota-indexer/README.md | 1 - crates/iota-indexer/tests/common/mod.rs | 29 +- .../tests/rpc-tests/indexer_api.rs | 534 +++++++++++++++++- .../tests/rpc-tests/move_utils.rs | 2 +- crates/test-cluster/src/lib.rs | 69 ++- 7 files changed, 620 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d5cb814c829..3942a8df648 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6806,6 +6806,7 @@ dependencies = [ "iota-rest-api", "iota-sdk 0.5.0-alpha", "iota-swarm-config", + "iota-test-transaction-builder", "iota-transaction-builder", "iota-types", "itertools 0.13.0", diff --git a/crates/iota-indexer/Cargo.toml b/crates/iota-indexer/Cargo.toml index 1532aa69dd8..6db4846a164 100644 --- a/crates/iota-indexer/Cargo.toml +++ b/crates/iota-indexer/Cargo.toml @@ -68,6 +68,8 @@ bundled-mysql = ["mysqlclient-sys?/bundled"] [dev-dependencies] # external dependencies rand.workspace = true +serde_json.workspace = true +serial_test = "2.0" # internal dependencies iota-config.workspace = true @@ -75,8 +77,7 @@ iota-genesis-builder.workspace = true iota-keys.workspace = true iota-move-build.workspace = true iota-swarm-config.workspace = true -serde_json.workspace = true -serial_test = "2.0" +iota-test-transaction-builder.workspace = true simulacrum.workspace = true test-cluster.workspace = true diff --git a/crates/iota-indexer/README.md b/crates/iota-indexer/README.md index 614f9601cb2..ffdcd0672a9 100644 --- a/crates/iota-indexer/README.md +++ b/crates/iota-indexer/README.md @@ -100,7 +100,6 @@ The crate provides following tests currently: > [!NOTE] > rpc tests which relies on postgres for every test it applies migrations, we need to run tests sequencially to avoid errors - ```sh # run tests requiring only postgres integration cargo nextest run --features pg_integration --test-threads 1 diff --git a/crates/iota-indexer/tests/common/mod.rs b/crates/iota-indexer/tests/common/mod.rs index b92cbbd25d9..208dde1ce21 100644 --- a/crates/iota-indexer/tests/common/mod.rs +++ b/crates/iota-indexer/tests/common/mod.rs @@ -18,8 +18,12 @@ use iota_indexer::{ test_utils::{ReaderWriterConfig, start_test_indexer}, }; use iota_json_rpc_api::ReadApiClient; +use iota_json_rpc_types::IotaTransactionBlockResponseOptions; use iota_metrics::init_metrics; -use iota_types::base_types::{ObjectID, SequenceNumber}; +use iota_types::{ + base_types::{ObjectID, SequenceNumber}, + digests::TransactionDigest, +}; use jsonrpsee::{ http_client::{HttpClient, HttpClientBuilder}, types::ErrorObject, @@ -148,6 +152,29 @@ pub async fn indexer_wait_for_object( .expect("Timeout waiting for indexer to catchup to given object's sequence number"); } +pub async fn indexer_wait_for_transaction( + tx_digest: TransactionDigest, + pg_store: &PgIndexerStore, + indexer_client: &HttpClient, +) { + tokio::time::timeout(Duration::from_secs(30), async { + loop { + if let Ok(tx) = indexer_client + .get_transaction_block(tx_digest, Some(IotaTransactionBlockResponseOptions::new())) + .await + { + if let Some(checkpoint) = tx.checkpoint { + indexer_wait_for_checkpoint(pg_store, checkpoint).await; + break; + } + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + }) + .await + .expect("Timeout waiting for indexer to catchup to given transaction"); +} + /// Start an Indexer instance in `Read` mode fn start_indexer_reader(fullnode_rpc_url: impl Into, data_ingestion_path: PathBuf) { let config = IndexerConfig { diff --git a/crates/iota-indexer/tests/rpc-tests/indexer_api.rs b/crates/iota-indexer/tests/rpc-tests/indexer_api.rs index c773356e5df..c830f7ae7a7 100644 --- a/crates/iota-indexer/tests/rpc-tests/indexer_api.rs +++ b/crates/iota-indexer/tests/rpc-tests/indexer_api.rs @@ -3,14 +3,36 @@ use std::{str::FromStr, time::SystemTime}; -use iota_json_rpc_api::IndexerApiClient; -use iota_json_rpc_types::{EventFilter, EventPage}; +use iota_json::{call_args, type_args}; +use iota_json_rpc_api::{IndexerApiClient, WriteApiClient}; +use iota_json_rpc_types::{ + EventFilter, EventPage, IotaMoveValue, IotaObjectDataFilter, IotaObjectDataOptions, + IotaObjectResponseQuery, IotaTransactionBlockResponseOptions, + IotaTransactionBlockResponseQuery, ObjectsPage, TransactionFilter, +}; +use iota_test_transaction_builder::TestTransactionBuilder; use iota_types::{ + IOTA_FRAMEWORK_ADDRESS, base_types::{IotaAddress, ObjectID}, + crypto::{AccountKeyPair, get_key_pair}, digests::TransactionDigest, + dynamic_field::DynamicFieldName, + gas_coin::GAS, + programmable_transaction_builder::ProgrammableTransactionBuilder, + quorum_driver_types::ExecuteTransactionRequestType, + transaction::{CallArg, Command, ObjectArg, TransactionData}, + utils::to_sender_signed_transaction, +}; +use move_core_types::{ + annotated_value::MoveValue, + identifier::Identifier, + language_storage::{StructTag, TypeTag}, }; -use crate::common::{ApiTestSetup, indexer_wait_for_checkpoint, rpc_call_error_msg_matches}; +use crate::common::{ + ApiTestSetup, indexer_wait_for_checkpoint, indexer_wait_for_object, + indexer_wait_for_transaction, rpc_call_error_msg_matches, +}; #[test] fn query_events_no_events_descending() { @@ -162,3 +184,509 @@ fn query_events_supported_events() { } }); } + +#[test] +fn test_get_owned_objects() -> Result<(), anyhow::Error> { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let address = cluster.get_address_0(); + + let objects = client + .get_owned_objects( + address, + Some(IotaObjectResponseQuery::new_with_options( + IotaObjectDataOptions::new(), + )), + None, + None, + ) + .await?; + assert_eq!(5, objects.data.len()); + + Ok(()) + }) +} + +#[test] +fn test_query_transaction_blocks_pagination() -> Result<(), anyhow::Error> { + let ApiTestSetup { + runtime, + store, + cluster, + client, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + let (address, keypair): (_, AccountKeyPair) = get_key_pair(); + + let gas_ref = cluster + .fund_address_and_return_gas( + cluster.get_reference_gas_price().await, + Some(500_000_000), + address, + ) + .await; + indexer_wait_for_object(client, gas_ref.0, gas_ref.1).await; + let coin_to_split = cluster + .fund_address_and_return_gas( + cluster.get_reference_gas_price().await, + Some(500_000_000), + address, + ) + .await; + indexer_wait_for_object(client, coin_to_split.0, coin_to_split.1).await; + let iota_client = cluster.wallet.get_client().await.unwrap(); + + let mut tx_responses = vec![]; + for _ in 0..5 { + let tx_data = iota_client + .transaction_builder() + .split_coin_equal(address, coin_to_split.0, 2, Some(gas_ref.0), 10_000_000) + .await?; + + let signed_transaction = to_sender_signed_transaction(tx_data, &keypair); + + let (tx_bytes, signatures) = signed_transaction.to_tx_bytes_and_signatures(); + + let res = client + .execute_transaction_block( + tx_bytes, + signatures, + Some(IotaTransactionBlockResponseOptions::new().with_effects()), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await?; + + tx_responses.push(res) + } + + let tx_res = tx_responses.pop().unwrap(); + + indexer_wait_for_transaction(tx_res.digest, store, client).await; + + let objects = client + .get_owned_objects( + address, + Some(IotaObjectResponseQuery::new_with_options( + IotaObjectDataOptions::new() + .with_type() + .with_owner() + .with_previous_transaction(), + )), + None, + None, + ) + .await? + .data; + + // 2 gas coins + 5 coins from the split + assert_eq!(7, objects.len()); + + // filter transactions by address + let query = IotaTransactionBlockResponseQuery { + options: Some(IotaTransactionBlockResponseOptions { + show_input: true, + show_effects: true, + show_events: true, + ..Default::default() + }), + filter: Some(TransactionFilter::FromAddress(address)), + }; + + let first_page = iota_client + .read_api() + .query_transaction_blocks(query.clone(), None, Some(3), true) + .await + .unwrap(); + assert_eq!(3, first_page.data.len()); + assert!(first_page.data[0].transaction.is_some()); + assert!(first_page.data[0].effects.is_some()); + assert!(first_page.data[0].events.is_some()); + assert!(first_page.has_next_page); + + // Read the next page for the last transaction + let next_page = iota_client + .read_api() + .query_transaction_blocks(query, first_page.next_cursor, None, true) + .await + .unwrap(); + + assert_eq!(2, next_page.data.len()); + assert!(next_page.data[0].transaction.is_some()); + assert!(next_page.data[0].effects.is_some()); + assert!(next_page.data[0].events.is_some()); + assert!(!next_page.has_next_page); + + Ok(()) + }) +} + +#[test] +fn test_query_transaction_blocks() -> Result<(), anyhow::Error> { + let ApiTestSetup { + runtime, + store, + cluster, + client, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + let (address, keypair): (_, AccountKeyPair) = get_key_pair(); + + let gas = cluster + .fund_address_and_return_gas( + cluster.get_reference_gas_price().await, + Some(500_000_000), + address, + ) + .await; + let coin_1 = cluster + .fund_address_and_return_gas( + cluster.get_reference_gas_price().await, + Some(500_000_000), + address, + ) + .await; + let coin_2 = cluster + .fund_address_and_return_gas( + cluster.get_reference_gas_price().await, + Some(500_000_000), + address, + ) + .await; + let iota_client = cluster.wallet.get_client().await.unwrap(); + + indexer_wait_for_object(client, gas.0, gas.1).await; + indexer_wait_for_object(client, coin_1.0, coin_1.1).await; + indexer_wait_for_object(client, coin_2.0, coin_2.1).await; + + let objects = client + .get_owned_objects( + address, + Some(IotaObjectResponseQuery::new_with_options( + IotaObjectDataOptions::new() + .with_type() + .with_owner() + .with_previous_transaction(), + )), + None, + None, + ) + .await? + .data; + + assert_eq!(objects.len(), 3); + + // make 2 move calls of same package & module, but different functions + let package_id = ObjectID::new(IOTA_FRAMEWORK_ADDRESS.into_bytes()); + let signer = address; + + let tx_builder = iota_client.transaction_builder().clone(); + let mut pt_builder = ProgrammableTransactionBuilder::new(); + + let module = Identifier::from_str("pay")?; + let function_1 = Identifier::from_str("split")?; + let function_2 = Identifier::from_str("divide_and_keep")?; + + let iota_type_args = type_args![GAS::type_tag()]?; + let type_args = iota_type_args + .into_iter() + .map(|ty| ty.try_into()) + .collect::, _>>()?; + + let iota_call_args_1 = call_args!(coin_1.0, 10)?; + let call_args_1 = tx_builder + .resolve_and_checks_json_args( + &mut pt_builder, + package_id, + &module, + &function_1, + &type_args, + iota_call_args_1, + ) + .await?; + let cmd_1 = Command::move_call( + package_id, + module.clone(), + function_1, + type_args.clone(), + call_args_1.clone(), + ); + + let iota_call_args_2 = call_args!(coin_2.0, 10)?; + let call_args_2 = tx_builder + .resolve_and_checks_json_args( + &mut pt_builder, + package_id, + &module, + &function_2, + &type_args, + iota_call_args_2, + ) + .await?; + let cmd_2 = Command::move_call(package_id, module, function_2, type_args, call_args_2); + pt_builder.command(cmd_1); + pt_builder.command(cmd_2); + let pt = pt_builder.finish(); + + let tx_data = TransactionData::new_programmable(signer, vec![gas], pt, 10_000_000, 1000); + + let signed_transaction = to_sender_signed_transaction(tx_data, &keypair); + + let response = iota_client + .quorum_driver_api() + .execute_transaction_block( + signed_transaction, + IotaTransactionBlockResponseOptions::new(), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await + .unwrap(); + + indexer_wait_for_transaction(response.digest, store, client).await; + + // match with None function, the DB should have 2 records, but both points to + // the same tx + let filter = TransactionFilter::FromAddress(signer); + let move_call_query = IotaTransactionBlockResponseQuery::new_with_filter(filter); + let res = client + .query_transaction_blocks(move_call_query, None, Some(20), Some(true)) + .await + .unwrap(); + + assert_eq!(1, res.data.len()); + + Ok(()) + }) +} + +#[test] +fn test_get_dynamic_fields() -> Result<(), anyhow::Error> { + let ApiTestSetup { + runtime, + store, + cluster, + client, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + let (address, keypair): (_, AccountKeyPair) = get_key_pair(); + + let gas = cluster + .fund_address_and_return_gas( + cluster.get_reference_gas_price().await, + Some(500_000_000_000), + address, + ) + .await; + indexer_wait_for_object(client, gas.0, gas.1).await; + + // Create a bag object + let pt = { + let mut builder = ProgrammableTransactionBuilder::new(); + let bag = builder.programmable_move_call( + ObjectID::new(IOTA_FRAMEWORK_ADDRESS.into_bytes()), + Identifier::from_str("bag")?, + Identifier::from_str("new")?, + vec![], + vec![], + ); + + let field_name_argument = builder.pure(0u64).expect("valid pure"); + let field_value_argument = builder.pure(0u64).expect("valid pure"); + + let _ = builder.programmable_move_call( + ObjectID::new(IOTA_FRAMEWORK_ADDRESS.into_bytes()), + Identifier::from_str("bag")?, + Identifier::from_str("add")?, + vec![TypeTag::U64, TypeTag::U64], + vec![bag, field_name_argument, field_value_argument], + ); + + builder.transfer_arg(address, bag); + builder.finish() + }; + + let tx_builder = TestTransactionBuilder::new(address, gas, 1000); + let tx_data = tx_builder.programmable(pt).build(); + let signed_transaction = to_sender_signed_transaction(tx_data, &keypair); + + let res = cluster + .wallet + .execute_transaction_must_succeed(signed_transaction) + .await; + + // Wait for the transaction to be executed + indexer_wait_for_transaction(res.digest, store, client).await; + + // Find the bag object + let objects: ObjectsPage = client + .get_owned_objects( + address, + Some(IotaObjectResponseQuery::new( + Some(IotaObjectDataFilter::StructType(StructTag { + address: IOTA_FRAMEWORK_ADDRESS, + module: Identifier::from_str("bag")?, + name: Identifier::from_str("Bag")?, + type_params: Vec::new(), + })), + Some( + IotaObjectDataOptions::new() + .with_type() + .with_owner() + .with_previous_transaction() + .with_display(), + ), + )), + None, + None, + ) + .await?; + + let bag_object_ref = objects.data.first().unwrap().object().unwrap().object_ref(); + + // Verify that the dynamic field was successfully added + let dynamic_fields = client + .get_dynamic_fields(bag_object_ref.0, None, None) + .await + .expect("Failed to get dynamic fields"); + + assert!( + !dynamic_fields.data.is_empty(), + "Dynamic field was not added" + ); + + Ok(()) + }) +} + +#[test] +fn test_get_dynamic_field_objects() -> Result<(), anyhow::Error> { + let ApiTestSetup { + runtime, + store, + cluster, + client, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + let (address, keypair): (_, AccountKeyPair) = get_key_pair(); + + let gas = cluster + .fund_address_and_return_gas( + cluster.get_reference_gas_price().await, + Some(500_000_000_000), + address, + ) + .await; + indexer_wait_for_object(client, gas.0, gas.1).await; + + let child_object = cluster + .fund_address_and_return_gas( + cluster.get_reference_gas_price().await, + Some(500_000_000), + address, + ) + .await; + + // Create a object bag object + let pt = { + let mut builder = ProgrammableTransactionBuilder::new(); + let bag = builder.programmable_move_call( + ObjectID::new(IOTA_FRAMEWORK_ADDRESS.into_bytes()), + Identifier::from_str("object_bag")?, + Identifier::from_str("new")?, + vec![], + vec![], + ); + + let field_name_argument = builder.pure(0u64).expect("valid pure"); + let field_value_argument = builder + .input(CallArg::Object(ObjectArg::ImmOrOwnedObject(child_object))) + .unwrap(); + + let _ = builder.programmable_move_call( + ObjectID::new(IOTA_FRAMEWORK_ADDRESS.into_bytes()), + Identifier::from_str("object_bag")?, + Identifier::from_str("add")?, + vec![ + TypeTag::U64, + TypeTag::Struct(Box::new(StructTag { + address: IOTA_FRAMEWORK_ADDRESS, + module: Identifier::from_str("coin")?, + name: Identifier::from_str("Coin")?, + type_params: vec![GAS::type_tag()], + })), + ], + vec![bag, field_name_argument, field_value_argument], + ); + + builder.transfer_arg(address, bag); + builder.finish() + }; + + let tx_builder = TestTransactionBuilder::new(address, gas, 1000); + let tx_data = tx_builder.programmable(pt).build(); + let signed_transaction = to_sender_signed_transaction(tx_data, &keypair); + + let res = cluster + .wallet + .execute_transaction_must_succeed(signed_transaction) + .await; + + // Wait for the transaction to be executed + indexer_wait_for_transaction(res.digest, store, client).await; + + // Find the bag object + let objects: ObjectsPage = client + .get_owned_objects( + address, + Some(IotaObjectResponseQuery::new( + Some(IotaObjectDataFilter::StructType(StructTag { + address: IOTA_FRAMEWORK_ADDRESS, + module: Identifier::from_str("object_bag")?, + name: Identifier::from_str("ObjectBag")?, + type_params: Vec::new(), + })), + Some( + IotaObjectDataOptions::new() + .with_type() + .with_owner() + .with_previous_transaction() + .with_display(), + ), + )), + None, + None, + ) + .await?; + + let bag_object_ref = objects.data.first().unwrap().object().unwrap().object_ref(); + + let name = DynamicFieldName { + type_: TypeTag::U64, + value: IotaMoveValue::from(MoveValue::U64(0u64)).to_json_value(), + }; + + // Verify that the dynamic field was successfully added + let dynamic_fields = client + .get_dynamic_field_object(bag_object_ref.0, name) + .await + .expect("Failed to get dynamic field object"); + + assert!( + dynamic_fields.data.is_some(), + "Dynamic field object was not added" + ); + + Ok(()) + }) +} diff --git a/crates/iota-indexer/tests/rpc-tests/move_utils.rs b/crates/iota-indexer/tests/rpc-tests/move_utils.rs index 0c4591c0038..f35c9fff673 100644 --- a/crates/iota-indexer/tests/rpc-tests/move_utils.rs +++ b/crates/iota-indexer/tests/rpc-tests/move_utils.rs @@ -4,7 +4,7 @@ use iota_json_rpc_api::MoveUtilsClient; use iota_json_rpc_types::{MoveFunctionArgType, ObjectValueKind}; -use crate::common::{indexer_wait_for_checkpoint, rpc_call_error_msg_matches, ApiTestSetup}; +use crate::common::{ApiTestSetup, indexer_wait_for_checkpoint, rpc_call_error_msg_matches}; #[test] fn get_move_function_arg_types_empty() { diff --git a/crates/test-cluster/src/lib.rs b/crates/test-cluster/src/lib.rs index c0167e5b082..682a15a5f81 100644 --- a/crates/test-cluster/src/lib.rs +++ b/crates/test-cluster/src/lib.rs @@ -70,7 +70,7 @@ use iota_types::{ get_bridge, get_bridge_obj_initial_shared_version, }, committee::{Committee, CommitteeTrait, EpochId}, - crypto::{IotaKeyPair, KeypairTraits, ToFromBytes}, + crypto::{AccountKeyPair, IotaKeyPair, KeypairTraits, ToFromBytes, get_key_pair}, effects::{TransactionEffects, TransactionEvents}, error::IotaResult, governance::MIN_VALIDATOR_JOINING_STAKE_NANOS, @@ -88,6 +88,7 @@ use iota_types::{ CertifiedTransaction, ObjectArg, Transaction, TransactionData, TransactionDataAPI, TransactionKind, }, + utils::to_sender_signed_transaction, }; use jsonrpsee::{ core::RpcResult, @@ -125,13 +126,18 @@ impl FullNodeHandle { } } +struct Faucet { + address: IotaAddress, + keypair: Arc>, +} + pub struct TestCluster { pub swarm: Swarm, pub wallet: WalletContext, pub fullnode_handle: FullNodeHandle, - pub bridge_authority_keys: Option>, pub bridge_server_ports: Option>, + faucet: Faucet, } impl TestCluster { @@ -798,7 +804,7 @@ impl TestCluster { )) } - /// This call sends some funds from the seeded address to the funding + /// This call sends some funds from the seeded faucet address to the funding /// address for the given amount and returns the gas object ref. This /// is useful to construct transactions from the funding address. pub async fn fund_address_and_return_gas( @@ -807,20 +813,43 @@ impl TestCluster { amount: Option, funding_address: IotaAddress, ) -> ObjectRef { - let context = &self.wallet; - let (sender, gas) = context.get_one_gas_object().await.unwrap().unwrap(); - let tx = context.sign_transaction( - &TestTransactionBuilder::new(sender, gas, rgp) - .transfer_iota(amount, funding_address) - .build(), - ); - context.execute_transaction_must_succeed(tx).await; + let Faucet { address, keypair } = &self.faucet; + + let keypair = &*keypair.lock().await; - context - .get_one_gas_object_owned_by_address(funding_address) + let gas_ref = *self + .wallet + .get_gas_objects_owned_by_address(*address, None) .await .unwrap() + .first() + .unwrap(); + + let tx_data = TestTransactionBuilder::new(*address, gas_ref, rgp) + .transfer_iota(amount, funding_address) + .build(); + + let signed_transaction = to_sender_signed_transaction(tx_data, keypair); + + let response = self + .iota_client() + .quorum_driver_api() + .execute_transaction_block( + signed_transaction, + IotaTransactionBlockResponseOptions::new().with_effects(), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await + .unwrap(); + + response + .effects + .unwrap() + .created() + .first() .unwrap() + .reference + .to_object_ref() } pub async fn transfer_iota_must_exceed( @@ -1273,6 +1302,14 @@ impl TestClusterBuilder { } pub async fn build(mut self) -> TestCluster { + // Add a faucet address + let (faucet_address, faucet_keypair): (IotaAddress, AccountKeyPair) = get_key_pair(); + let accounts = &mut self.get_or_init_genesis_config().accounts; + accounts.push(AccountConfig { + address: Some(faucet_address), + gas_amounts: vec![DEFAULT_GAS_AMOUNT], + }); + // All test clusters receive a continuous stream of random JWKs. // If we later use zklogin authenticated transactions in tests we will need to // supply valid JWKs as well. @@ -1335,6 +1372,12 @@ impl TestClusterBuilder { fullnode_handle, bridge_authority_keys: None, bridge_server_ports: None, + faucet: Faucet { + address: faucet_address, + keypair: Arc::new(tokio::sync::Mutex::new(IotaKeyPair::Ed25519( + faucet_keypair, + ))), + }, } } From 16a9510dfbd6bfd557a032da53e8750b4efe13b7 Mon Sep 17 00:00:00 2001 From: Samuel Rufinatscha Date: Mon, 28 Oct 2024 19:47:49 +0100 Subject: [PATCH 09/24] test(iota-indexer): Add tests for `GovernanceApi` (#2825) * refactor: Rebase Governance API tests * feat: Implement simple test cases * refactor: Fmt * refactor: Assert system state summary; assert reference gas price * refactor: Try add timelocked stakes to the cluster * fix: add timelocked staking test * fix: add timelocked unstaking test * refactor: wait for transaction in timelocked tests * refactor: fix clippy * refactor: fix clippy * refactor: fix clippy * refactor: remove test that is not in feature base branch * refactor: fix fmt * refactor: fix fmt * refactor: use dedicated keypair for timelocked staking tests --- crates/iota-indexer/tests/common/mod.rs | 16 + .../tests/rpc-tests/governance_api.rs | 596 ++++++++++++++++++ crates/iota-indexer/tests/rpc-tests/main.rs | 5 +- 3 files changed, 614 insertions(+), 3 deletions(-) create mode 100644 crates/iota-indexer/tests/rpc-tests/governance_api.rs diff --git a/crates/iota-indexer/tests/common/mod.rs b/crates/iota-indexer/tests/common/mod.rs index 208dde1ce21..c6950a5e60c 100644 --- a/crates/iota-indexer/tests/common/mod.rs +++ b/crates/iota-indexer/tests/common/mod.rs @@ -127,6 +127,22 @@ pub async fn indexer_wait_for_checkpoint( .expect("Timeout waiting for indexer to catchup to checkpoint"); } +/// Wait for the indexer to catch up to the latest node checkpoint sequence +/// number. Indexer starts storing data after checkpoint 0 +pub async fn indexer_wait_for_latest_checkpoint( + pg_store: &PgIndexerStore, + cluster: &TestCluster, +) { + let latest_checkpoint = cluster + .iota_client() + .read_api() + .get_latest_checkpoint_sequence_number() + .await + .unwrap(); + + indexer_wait_for_checkpoint(pg_store, latest_checkpoint).await; +} + /// Wait for the indexer to catch up to the given object sequence number pub async fn indexer_wait_for_object( client: &HttpClient, diff --git a/crates/iota-indexer/tests/rpc-tests/governance_api.rs b/crates/iota-indexer/tests/rpc-tests/governance_api.rs new file mode 100644 index 00000000000..79fc5803430 --- /dev/null +++ b/crates/iota-indexer/tests/rpc-tests/governance_api.rs @@ -0,0 +1,596 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use iota_json_rpc_api::{ + CoinReadApiClient, GovernanceReadApiClient, IndexerApiClient, TransactionBuilderClient, + WriteApiClient, +}; +use iota_json_rpc_types::{ + CoinPage, DelegatedStake, IotaObjectDataOptions, IotaObjectResponseQuery, + IotaTransactionBlockResponseOptions, ObjectsPage, StakeStatus, TransactionBlockBytes, +}; +use iota_test_transaction_builder::TestTransactionBuilder; +use iota_types::{ + IOTA_FRAMEWORK_ADDRESS, IOTA_SYSTEM_ADDRESS, + balance::Balance, + base_types::ObjectID, + crypto::{AccountKeyPair, get_key_pair}, + gas_coin::GAS, + programmable_transaction_builder::ProgrammableTransactionBuilder, + quorum_driver_types::ExecuteTransactionRequestType, + transaction::{CallArg, ObjectArg}, + utils::to_sender_signed_transaction, +}; +use move_core_types::{identifier::Identifier, language_storage::TypeTag}; + +use crate::common::{ + ApiTestSetup, indexer_wait_for_checkpoint, indexer_wait_for_latest_checkpoint, + indexer_wait_for_object, indexer_wait_for_transaction, +}; + +#[test] +fn test_staking() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let address = cluster.get_address_0(); + + let objects: ObjectsPage = client + .get_owned_objects( + address, + Some(IotaObjectResponseQuery::new_with_options( + IotaObjectDataOptions::new() + .with_type() + .with_owner() + .with_previous_transaction(), + )), + None, + None, + ) + .await + .unwrap(); + assert_eq!(5, objects.data.len()); + + // Check StakedIota object before test + let staked_iota: Vec = client.get_stakes(address).await.unwrap(); + assert!(staked_iota.is_empty()); + + let validator = client + .get_latest_iota_system_state() + .await + .unwrap() + .active_validators[0] + .iota_address; + + let coin = objects.data[0].object().unwrap().object_id; + // Delegate some IOTA + let transaction_bytes: TransactionBlockBytes = client + .request_add_stake( + address, + vec![coin], + Some(1000000000.into()), + validator, + None, + 100_000_000.into(), + ) + .await + .unwrap(); + let tx = cluster + .wallet + .sign_transaction(&transaction_bytes.to_data().unwrap()); + + let (tx_bytes, signatures) = tx.to_tx_bytes_and_signatures(); + + client + .execute_transaction_block( + tx_bytes, + signatures, + Some(IotaTransactionBlockResponseOptions::new()), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await + .unwrap(); + + cluster.force_new_epoch().await; + indexer_wait_for_latest_checkpoint(store, cluster).await; + + // Check DelegatedStake object after epoch transition + let staked_iota: Vec = client.get_stakes(address).await.unwrap(); + assert_eq!(1, staked_iota.len()); + assert_eq!(1000000000, staked_iota[0].stakes[0].principal); + assert!(matches!( + staked_iota[0].stakes[0].status, + StakeStatus::Active { + estimated_reward: _ + } + )); + }); +} + +#[test] +fn test_unstaking() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + + let indexer_client = client; + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let address = cluster.get_address_0(); + + let coins: CoinPage = indexer_client + .get_coins(address, None, None, None) + .await + .unwrap(); + assert_eq!(5, coins.data.len()); + + // Check StakedIota object before test + let staked_iota: Vec = indexer_client.get_stakes(address).await.unwrap(); + assert!(staked_iota.is_empty()); + + let validator = indexer_client + .get_latest_iota_system_state() + .await + .unwrap() + .active_validators[0] + .iota_address; + + // Delegate some IOTA + let transaction_bytes: TransactionBlockBytes = indexer_client + .request_add_stake( + address, + vec![coins.data[0].coin_object_id], + Some(1000000000.into()), + validator, + None, + 100_000_000.into(), + ) + .await + .unwrap(); + let tx = cluster + .wallet + .sign_transaction(&transaction_bytes.to_data().unwrap()); + + let (tx_bytes, signatures) = tx.to_tx_bytes_and_signatures(); + + indexer_client + .execute_transaction_block( + tx_bytes, + signatures, + Some(IotaTransactionBlockResponseOptions::new()), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await + .unwrap(); + + cluster.force_new_epoch().await; + indexer_wait_for_latest_checkpoint(store, cluster).await; + + // Check DelegatedStake object + let staked_iota: Vec = indexer_client.get_stakes(address).await.unwrap(); + assert_eq!(1, staked_iota.len()); + assert_eq!(1000000000, staked_iota[0].stakes[0].principal); + assert!(matches!( + staked_iota[0].stakes[0].status, + StakeStatus::Active { + estimated_reward: _ + } + )); + + let transaction_bytes: TransactionBlockBytes = indexer_client + .request_withdraw_stake( + address, + staked_iota[0].stakes[0].staked_iota_id, + None, + 100_000_000.into(), + ) + .await + .unwrap(); + let tx = cluster + .wallet + .sign_transaction(&transaction_bytes.to_data().unwrap()); + + let _ = cluster.wallet.execute_transaction_must_succeed(tx).await; + + cluster.force_new_epoch().await; + indexer_wait_for_latest_checkpoint(store, cluster).await; + + let node_response = cluster + .rpc_client() + .get_stakes_by_ids(vec![staked_iota[0].stakes[0].staked_iota_id]) + .await + .unwrap(); + assert_eq!(1, node_response.len()); + assert!(matches!( + node_response[0].stakes[0].status, + StakeStatus::Unstaked + )); + + let indexer_response = indexer_client + .get_stakes_by_ids(vec![staked_iota[0].stakes[0].staked_iota_id]) + .await + .unwrap(); + assert_eq!(0, indexer_response.len()); + + let staked_iota: Vec = indexer_client.get_stakes(address).await.unwrap(); + assert!(staked_iota.is_empty()); + }); +} + +#[test] +fn test_timelocked_staking() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let (sender, keypair): (_, AccountKeyPair) = get_key_pair(); + + let gas = cluster + .fund_address_and_return_gas( + cluster.get_reference_gas_price().await, + Some(10_000_000_000), + sender, + ) + .await; + + indexer_wait_for_object(client, gas.0, gas.1).await; + + let iota_coin_ref = cluster + .fund_address_and_return_gas( + cluster.get_reference_gas_price().await, + Some(10_000_000_000), + sender, + ) + .await; + + indexer_wait_for_object(client, iota_coin_ref.0, iota_coin_ref.1).await; + + let pt = { + let mut builder = ProgrammableTransactionBuilder::new(); + + let iota_coin_argument = builder + .input(CallArg::Object(ObjectArg::ImmOrOwnedObject(iota_coin_ref))) + .expect("valid obj"); + + // Step 1: Get the IOTA balance from the coin object. + let iota_balance = builder.programmable_move_call( + ObjectID::new(IOTA_FRAMEWORK_ADDRESS.into_bytes()), + Identifier::new("coin").unwrap(), + Identifier::new("into_balance").unwrap(), + vec![GAS::type_tag()], + vec![iota_coin_argument], + ); + + // Step 2: Timelock the IOTA balance. + let timelock_timestamp = builder.input(CallArg::from(u64::MAX)).unwrap(); + let timelocked_iota_balance = builder.programmable_move_call( + ObjectID::new(IOTA_FRAMEWORK_ADDRESS.into_bytes()), + Identifier::new("timelock").unwrap(), + Identifier::new("lock").unwrap(), + vec![TypeTag::Struct(Box::new(Balance::type_(GAS::type_tag())))], + vec![iota_balance, timelock_timestamp], + ); + + // Step 3: Delegate the timelocked IOTA balance. + let validator = client + .get_latest_iota_system_state() + .await + .unwrap() + .active_validators[0] + .iota_address; + + let validator = builder + .input(CallArg::Pure(bcs::to_bytes(&validator).unwrap())) + .unwrap(); + let state = builder.input(CallArg::IOTA_SYSTEM_MUT).unwrap(); + + let _ = builder.programmable_move_call( + ObjectID::new(IOTA_SYSTEM_ADDRESS.into_bytes()), + Identifier::new("timelocked_staking").unwrap(), + Identifier::new("request_add_stake").unwrap(), + vec![], + vec![state, timelocked_iota_balance, validator], + ); + + builder.finish() + }; + + let context = &cluster.wallet; + let gas_price = context.get_reference_gas_price().await.unwrap(); + + let tx_builder = TestTransactionBuilder::new(sender, gas, gas_price); + let txn = to_sender_signed_transaction(tx_builder.programmable(pt).build(), &keypair); + + let res = context.execute_transaction_must_succeed(txn).await; + indexer_wait_for_transaction(res.digest, store, client).await; + + cluster.force_new_epoch().await; + indexer_wait_for_latest_checkpoint(store, cluster).await; + + let response = client.get_timelocked_stakes(sender).await.unwrap(); + + assert_eq!(response.len(), 1); + }); +} + +#[test] +fn test_timelocked_unstaking() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let (sender, keypair): (_, AccountKeyPair) = get_key_pair(); + + let gas = cluster + .fund_address_and_return_gas( + cluster.get_reference_gas_price().await, + Some(10_000_000_000), + sender, + ) + .await; + + indexer_wait_for_object(client, gas.0, gas.1).await; + + let iota_coin_ref = cluster + .fund_address_and_return_gas( + cluster.get_reference_gas_price().await, + Some(10_000_000_000), + sender, + ) + .await; + + indexer_wait_for_object(client, iota_coin_ref.0, iota_coin_ref.1).await; + + let pt = { + let mut builder = ProgrammableTransactionBuilder::new(); + + let iota_coin_argument = builder + .input(CallArg::Object(ObjectArg::ImmOrOwnedObject(iota_coin_ref))) + .expect("valid obj"); + + // Step 1: Get the IOTA balance from the coin object. + let iota_balance = builder.programmable_move_call( + ObjectID::new(IOTA_FRAMEWORK_ADDRESS.into_bytes()), + Identifier::new("coin").unwrap(), + Identifier::new("into_balance").unwrap(), + vec![GAS::type_tag()], + vec![iota_coin_argument], + ); + + // Step 2: Timelock the IOTA balance. + let timelock_timestamp = builder.input(CallArg::from(u64::MAX)).unwrap(); + let timelocked_iota_balance = builder.programmable_move_call( + ObjectID::new(IOTA_FRAMEWORK_ADDRESS.into_bytes()), + Identifier::new("timelock").unwrap(), + Identifier::new("lock").unwrap(), + vec![TypeTag::Struct(Box::new(Balance::type_(GAS::type_tag())))], + vec![iota_balance, timelock_timestamp], + ); + + // Step 3: Delegate the timelocked IOTA balance. + let validator = client + .get_latest_iota_system_state() + .await + .unwrap() + .active_validators[0] + .iota_address; + + let validator = builder + .input(CallArg::Pure(bcs::to_bytes(&validator).unwrap())) + .unwrap(); + let state = builder.input(CallArg::IOTA_SYSTEM_MUT).unwrap(); + + let _ = builder.programmable_move_call( + ObjectID::new(IOTA_SYSTEM_ADDRESS.into_bytes()), + Identifier::new("timelocked_staking").unwrap(), + Identifier::new("request_add_stake").unwrap(), + vec![], + vec![state, timelocked_iota_balance, validator], + ); + + builder.finish() + }; + + let context = &cluster.wallet; + let gas_price = context.get_reference_gas_price().await.unwrap(); + + let tx_builder = TestTransactionBuilder::new(sender, gas, gas_price); + let txn = to_sender_signed_transaction(tx_builder.programmable(pt).build(), &keypair); + + let res = context.execute_transaction_must_succeed(txn).await; + indexer_wait_for_transaction(res.digest, store, client).await; + + cluster.force_new_epoch().await; + indexer_wait_for_latest_checkpoint(store, cluster).await; + + let response = client.get_timelocked_stakes(sender).await.unwrap(); + + assert_eq!(response.len(), 1); + + let timelocked_stake_id = response[0].stakes[0].timelocked_staked_iota_id; + let timelocked_stake_id_ref = cluster + .wallet + .get_object_ref(timelocked_stake_id) + .await + .unwrap(); + + let pt = { + let mut builder = ProgrammableTransactionBuilder::new(); + + let timelocked_stake_id_argument = builder + .input(CallArg::Object(ObjectArg::ImmOrOwnedObject( + timelocked_stake_id_ref, + ))) + .expect("valid obj"); + + let state = builder.input(CallArg::IOTA_SYSTEM_MUT).unwrap(); + + let _ = builder.programmable_move_call( + ObjectID::new(IOTA_SYSTEM_ADDRESS.into_bytes()), + Identifier::new("timelocked_staking").unwrap(), + Identifier::new("request_withdraw_stake").unwrap(), + vec![], + vec![state, timelocked_stake_id_argument], + ); + + builder.finish() + }; + + let gas = cluster.wallet.get_object_ref(gas.0).await.unwrap(); + let tx_builder = TestTransactionBuilder::new(sender, gas, gas_price); + let txn = to_sender_signed_transaction(tx_builder.programmable(pt).build(), &keypair); + + let res = context.execute_transaction_must_succeed(txn).await; + indexer_wait_for_transaction(res.digest, store, client).await; + + cluster.force_new_epoch().await; + indexer_wait_for_latest_checkpoint(store, cluster).await; + + let res = client.get_timelocked_stakes(sender).await.unwrap(); + assert_eq!(res.len(), 0); + + let res = cluster + .rpc_client() + .get_timelocked_stakes_by_ids(vec![timelocked_stake_id]) + .await + .unwrap(); + + assert_eq!(res.len(), 1); + + assert!(matches!(res[0].stakes[0].status, StakeStatus::Unstaked)); + + let res = client + .get_timelocked_stakes_by_ids(vec![timelocked_stake_id]) + .await + .unwrap(); + + assert_eq!(res.len(), 0); + }); +} + +#[test] +fn get_latest_iota_system_state() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let response = client.get_latest_iota_system_state().await.unwrap(); + assert_eq!(response.epoch, 0); + assert_eq!(response.protocol_version, 1); + assert_eq!(response.system_state_version, 1); + }); +} + +#[test] +fn get_committee_info() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + // Test with no specified epoch + let response = client.get_committee_info(None).await.unwrap(); + + let (epoch_id, validators) = (response.epoch, response.validators); + + assert!(epoch_id == 0); + assert_eq!(validators.len(), 4); + + // Test with specified epoch 0 + let response = client.get_committee_info(Some(0.into())).await.unwrap(); + + let (epoch_id, validators) = (response.epoch, response.validators); + + assert!(epoch_id == 0); + assert_eq!(validators.len(), 4); + + // Test with non-existent epoch + let response = client.get_committee_info(Some(1.into())).await; + + assert!(response.is_err()); + + // Sleep for 5 seconds + cluster.force_new_epoch().await; + + // Test with specified epoch 1 + let response = client.get_committee_info(Some(1.into())).await.unwrap(); + + let (epoch_id, validators) = (response.epoch, response.validators); + + assert!(epoch_id == 1); + assert_eq!(validators.len(), 4); + }); +} + +#[test] +fn get_reference_gas_price() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let response = client.get_reference_gas_price().await.unwrap(); + assert_eq!(response, 1000.into()); + }); +} + +#[test] +fn get_validators_apy() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(store, 1).await; + + let response = client.get_validators_apy().await.unwrap(); + let (apys, epoch) = (response.apys, response.epoch); + + assert_eq!(epoch, 0); + assert_eq!(apys.len(), 4); + assert!(apys.iter().any(|apy| apy.apy == 0.0)); + }); +} diff --git a/crates/iota-indexer/tests/rpc-tests/main.rs b/crates/iota-indexer/tests/rpc-tests/main.rs index ccf62fbf0a6..5c7dd6490c7 100644 --- a/crates/iota-indexer/tests/rpc-tests/main.rs +++ b/crates/iota-indexer/tests/rpc-tests/main.rs @@ -4,13 +4,12 @@ #[allow(dead_code)] #[path = "../common/mod.rs"] mod common; - #[cfg(feature = "pg_integration")] mod extended_api; - +#[cfg(feature = "shared_test_runtime")] +mod governance_api; #[cfg(feature = "shared_test_runtime")] mod indexer_api; - #[cfg(feature = "shared_test_runtime")] mod move_utils; From a5f3e2debacff61e04e8377d35eb526d31f44b9a Mon Sep 17 00:00:00 2001 From: Samuel Rufinatscha Date: Tue, 29 Oct 2024 12:47:45 +0100 Subject: [PATCH 10/24] (Indexer): Use dedicated keypairs for Governance API tests (#3745) * refactor: use dedicated keypairs and consider epoch progression from other tests * refactor: remove unnecessary gas_price * 2 --- .../tests/rpc-tests/governance_api.rs | 171 ++++++++---------- 1 file changed, 72 insertions(+), 99 deletions(-) diff --git a/crates/iota-indexer/tests/rpc-tests/governance_api.rs b/crates/iota-indexer/tests/rpc-tests/governance_api.rs index 79fc5803430..c59c6637e49 100644 --- a/crates/iota-indexer/tests/rpc-tests/governance_api.rs +++ b/crates/iota-indexer/tests/rpc-tests/governance_api.rs @@ -1,14 +1,8 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use iota_json_rpc_api::{ - CoinReadApiClient, GovernanceReadApiClient, IndexerApiClient, TransactionBuilderClient, - WriteApiClient, -}; -use iota_json_rpc_types::{ - CoinPage, DelegatedStake, IotaObjectDataOptions, IotaObjectResponseQuery, - IotaTransactionBlockResponseOptions, ObjectsPage, StakeStatus, TransactionBlockBytes, -}; +use iota_json_rpc_api::{GovernanceReadApiClient, TransactionBuilderClient}; +use iota_json_rpc_types::{DelegatedStake, StakeStatus, TransactionBlockBytes}; use iota_test_transaction_builder::TestTransactionBuilder; use iota_types::{ IOTA_FRAMEWORK_ADDRESS, IOTA_SYSTEM_ADDRESS, @@ -17,7 +11,6 @@ use iota_types::{ crypto::{AccountKeyPair, get_key_pair}, gas_coin::GAS, programmable_transaction_builder::ProgrammableTransactionBuilder, - quorum_driver_types::ExecuteTransactionRequestType, transaction::{CallArg, ObjectArg}, utils::to_sender_signed_transaction, }; @@ -40,26 +33,30 @@ fn test_staking() { runtime.block_on(async move { indexer_wait_for_checkpoint(store, 1).await; - let address = cluster.get_address_0(); - - let objects: ObjectsPage = client - .get_owned_objects( - address, - Some(IotaObjectResponseQuery::new_with_options( - IotaObjectDataOptions::new() - .with_type() - .with_owner() - .with_previous_transaction(), - )), - None, - None, + let (sender, keypair): (_, AccountKeyPair) = get_key_pair(); + + let gas = cluster + .fund_address_and_return_gas( + cluster.get_reference_gas_price().await, + Some(10_000_000_000), + sender, ) - .await - .unwrap(); - assert_eq!(5, objects.data.len()); + .await; + + indexer_wait_for_object(client, gas.0, gas.1).await; + + let iota_coin_ref = cluster + .fund_address_and_return_gas( + cluster.get_reference_gas_price().await, + Some(10_000_000_000), + sender, + ) + .await; + + indexer_wait_for_object(client, iota_coin_ref.0, iota_coin_ref.1).await; // Check StakedIota object before test - let staked_iota: Vec = client.get_stakes(address).await.unwrap(); + let staked_iota: Vec = client.get_stakes(sender).await.unwrap(); assert!(staked_iota.is_empty()); let validator = client @@ -69,40 +66,29 @@ fn test_staking() { .active_validators[0] .iota_address; - let coin = objects.data[0].object().unwrap().object_id; // Delegate some IOTA let transaction_bytes: TransactionBlockBytes = client .request_add_stake( - address, - vec![coin], + sender, + vec![iota_coin_ref.0], Some(1000000000.into()), validator, - None, + Some(gas.0), 100_000_000.into(), ) .await .unwrap(); - let tx = cluster - .wallet - .sign_transaction(&transaction_bytes.to_data().unwrap()); - let (tx_bytes, signatures) = tx.to_tx_bytes_and_signatures(); + let txn = to_sender_signed_transaction(transaction_bytes.to_data().unwrap(), &keypair); - client - .execute_transaction_block( - tx_bytes, - signatures, - Some(IotaTransactionBlockResponseOptions::new()), - Some(ExecuteTransactionRequestType::WaitForLocalExecution), - ) - .await - .unwrap(); + let res = cluster.wallet.execute_transaction_must_succeed(txn).await; + indexer_wait_for_transaction(res.digest, store, client).await; cluster.force_new_epoch().await; indexer_wait_for_latest_checkpoint(store, cluster).await; // Check DelegatedStake object after epoch transition - let staked_iota: Vec = client.get_stakes(address).await.unwrap(); + let staked_iota: Vec = client.get_stakes(sender).await.unwrap(); assert_eq!(1, staked_iota.len()); assert_eq!(1000000000, staked_iota[0].stakes[0].principal); assert!(matches!( @@ -128,16 +114,30 @@ fn test_unstaking() { runtime.block_on(async move { indexer_wait_for_checkpoint(store, 1).await; - let address = cluster.get_address_0(); + let (sender, keypair): (_, AccountKeyPair) = get_key_pair(); - let coins: CoinPage = indexer_client - .get_coins(address, None, None, None) - .await - .unwrap(); - assert_eq!(5, coins.data.len()); + let gas = cluster + .fund_address_and_return_gas( + cluster.get_reference_gas_price().await, + Some(10_000_000_000), + sender, + ) + .await; + + indexer_wait_for_object(client, gas.0, gas.1).await; + + let iota_coin_ref = cluster + .fund_address_and_return_gas( + cluster.get_reference_gas_price().await, + Some(10_000_000_000), + sender, + ) + .await; + + indexer_wait_for_object(client, iota_coin_ref.0, iota_coin_ref.1).await; // Check StakedIota object before test - let staked_iota: Vec = indexer_client.get_stakes(address).await.unwrap(); + let staked_iota: Vec = indexer_client.get_stakes(sender).await.unwrap(); assert!(staked_iota.is_empty()); let validator = indexer_client @@ -150,36 +150,26 @@ fn test_unstaking() { // Delegate some IOTA let transaction_bytes: TransactionBlockBytes = indexer_client .request_add_stake( - address, - vec![coins.data[0].coin_object_id], + sender, + vec![iota_coin_ref.0], Some(1000000000.into()), validator, - None, + Some(gas.0), 100_000_000.into(), ) .await .unwrap(); - let tx = cluster - .wallet - .sign_transaction(&transaction_bytes.to_data().unwrap()); - let (tx_bytes, signatures) = tx.to_tx_bytes_and_signatures(); + let txn = to_sender_signed_transaction(transaction_bytes.to_data().unwrap(), &keypair); - indexer_client - .execute_transaction_block( - tx_bytes, - signatures, - Some(IotaTransactionBlockResponseOptions::new()), - Some(ExecuteTransactionRequestType::WaitForLocalExecution), - ) - .await - .unwrap(); + let res = cluster.wallet.execute_transaction_must_succeed(txn).await; + indexer_wait_for_transaction(res.digest, store, client).await; cluster.force_new_epoch().await; indexer_wait_for_latest_checkpoint(store, cluster).await; // Check DelegatedStake object - let staked_iota: Vec = indexer_client.get_stakes(address).await.unwrap(); + let staked_iota: Vec = indexer_client.get_stakes(sender).await.unwrap(); assert_eq!(1, staked_iota.len()); assert_eq!(1000000000, staked_iota[0].stakes[0].principal); assert!(matches!( @@ -191,18 +181,18 @@ fn test_unstaking() { let transaction_bytes: TransactionBlockBytes = indexer_client .request_withdraw_stake( - address, + sender, staked_iota[0].stakes[0].staked_iota_id, - None, + Some(gas.0), 100_000_000.into(), ) .await .unwrap(); - let tx = cluster - .wallet - .sign_transaction(&transaction_bytes.to_data().unwrap()); - let _ = cluster.wallet.execute_transaction_must_succeed(tx).await; + let txn = to_sender_signed_transaction(transaction_bytes.to_data().unwrap(), &keypair); + + let res = cluster.wallet.execute_transaction_must_succeed(txn).await; + indexer_wait_for_transaction(res.digest, store, client).await; cluster.force_new_epoch().await; indexer_wait_for_latest_checkpoint(store, cluster).await; @@ -224,7 +214,7 @@ fn test_unstaking() { .unwrap(); assert_eq!(0, indexer_response.len()); - let staked_iota: Vec = indexer_client.get_stakes(address).await.unwrap(); + let staked_iota: Vec = indexer_client.get_stakes(sender).await.unwrap(); assert!(staked_iota.is_empty()); }); } @@ -504,10 +494,9 @@ fn get_latest_iota_system_state() { runtime.block_on(async move { indexer_wait_for_checkpoint(store, 1).await; - let response = client.get_latest_iota_system_state().await.unwrap(); - assert_eq!(response.epoch, 0); - assert_eq!(response.protocol_version, 1); - assert_eq!(response.system_state_version, 1); + let system_state = client.get_latest_iota_system_state().await.unwrap(); + assert_eq!(system_state.protocol_version, 1); + assert_eq!(system_state.system_state_version, 1); }); } @@ -517,7 +506,7 @@ fn get_committee_info() { runtime, store, client, - cluster, + .. } = ApiTestSetup::get_or_init(); runtime.block_on(async move { @@ -526,10 +515,7 @@ fn get_committee_info() { // Test with no specified epoch let response = client.get_committee_info(None).await.unwrap(); - let (epoch_id, validators) = (response.epoch, response.validators); - - assert!(epoch_id == 0); - assert_eq!(validators.len(), 4); + assert_eq!(response.validators.len(), 4); // Test with specified epoch 0 let response = client.get_committee_info(Some(0.into())).await.unwrap(); @@ -540,20 +526,9 @@ fn get_committee_info() { assert_eq!(validators.len(), 4); // Test with non-existent epoch - let response = client.get_committee_info(Some(1.into())).await; + let response = client.get_committee_info(Some(u64::MAX.into())).await; assert!(response.is_err()); - - // Sleep for 5 seconds - cluster.force_new_epoch().await; - - // Test with specified epoch 1 - let response = client.get_committee_info(Some(1.into())).await.unwrap(); - - let (epoch_id, validators) = (response.epoch, response.validators); - - assert!(epoch_id == 1); - assert_eq!(validators.len(), 4); }); } @@ -586,10 +561,8 @@ fn get_validators_apy() { runtime.block_on(async move { indexer_wait_for_checkpoint(store, 1).await; - let response = client.get_validators_apy().await.unwrap(); - let (apys, epoch) = (response.apys, response.epoch); + let apys = client.get_validators_apy().await.unwrap().apys; - assert_eq!(epoch, 0); assert_eq!(apys.len(), 4); assert!(apys.iter().any(|apy| apy.apy == 0.0)); }); From b6b1675a70510320178b0c4f6e1cfc28e9d97194 Mon Sep 17 00:00:00 2001 From: tomxey Date: Tue, 29 Oct 2024 15:49:55 +0100 Subject: [PATCH 11/24] feat(iota-indexer): Refactor ExtendedApi tests in indexer to use shared test cluster (#3018) * feat(iota-indexer): Refactor ExtendedApi tests in indexer to use shared test cluster * Reuse function for finding available port/socket * Get rid of the `replace_db_name` function * Use &str instad of String for database_name where possible * Remove SimulacrumApiTestEnvDefinition type, rename InitializedSimulacrumEnv type * Fixes after rebase on recent feature branch * Update README to run tests with `--test-threads 1` --- crates/iota-cluster-test/src/cluster.rs | 2 + .../src/test_infra/cluster.rs | 2 + crates/iota-indexer/README.md | 2 +- crates/iota-indexer/src/test_utils.rs | 12 +- crates/iota-indexer/tests/common/mod.rs | 124 ++- crates/iota-indexer/tests/ingestion_tests.rs | 18 +- .../tests/rpc-tests/extended_api.rs | 741 +++++++++--------- crates/iota-indexer/tests/rpc-tests/main.rs | 2 +- 8 files changed, 478 insertions(+), 425 deletions(-) diff --git a/crates/iota-cluster-test/src/cluster.rs b/crates/iota-cluster-test/src/cluster.rs index e493e73f4bc..1196961d9db 100644 --- a/crates/iota-cluster-test/src/cluster.rs +++ b/crates/iota-cluster-test/src/cluster.rs @@ -265,6 +265,7 @@ impl Cluster for LocalNewCluster { fullnode_url.clone(), ReaderWriterConfig::writer_mode(None), data_ingestion_path.clone(), + None, ) .await; @@ -274,6 +275,7 @@ impl Cluster for LocalNewCluster { fullnode_url.clone(), ReaderWriterConfig::reader_mode(indexer_address.to_string()), data_ingestion_path, + None, ) .await; } diff --git a/crates/iota-graphql-rpc/src/test_infra/cluster.rs b/crates/iota-graphql-rpc/src/test_infra/cluster.rs index 79d905cf1ff..36aff4343d1 100644 --- a/crates/iota-graphql-rpc/src/test_infra/cluster.rs +++ b/crates/iota-graphql-rpc/src/test_infra/cluster.rs @@ -73,6 +73,7 @@ pub async fn start_cluster( true, Some(data_ingestion_path), cancellation_token.clone(), + None, ) .await; @@ -137,6 +138,7 @@ pub async fn serve_executor( true, Some(data_ingestion_path), cancellation_token.clone(), + Some(&graphql_connection_config.db_name()), ) .await; diff --git a/crates/iota-indexer/README.md b/crates/iota-indexer/README.md index ffdcd0672a9..f19bead54e7 100644 --- a/crates/iota-indexer/README.md +++ b/crates/iota-indexer/README.md @@ -104,7 +104,7 @@ The crate provides following tests currently: # run tests requiring only postgres integration cargo nextest run --features pg_integration --test-threads 1 # run rpc tests with shared runtime -cargo test --features shared_test_runtime +cargo test --features shared_test_runtime -- --test-threads 1 ``` For a better testing experience is possible to use [nextest](https://nexte.st/) diff --git a/crates/iota-indexer/src/test_utils.rs b/crates/iota-indexer/src/test_utils.rs index fb00f19caba..4058acc5634 100644 --- a/crates/iota-indexer/src/test_utils.rs +++ b/crates/iota-indexer/src/test_utils.rs @@ -45,6 +45,7 @@ pub async fn start_test_indexer( rpc_url: String, reader_writer_config: ReaderWriterConfig, data_ingestion_path: PathBuf, + new_database: Option<&str>, ) -> (PgIndexerStore, JoinHandle>) { start_test_indexer_impl( db_url, @@ -54,6 +55,7 @@ pub async fn start_test_indexer( false, Some(data_ingestion_path), CancellationToken::new(), + new_database, ) .await } @@ -65,17 +67,23 @@ pub async fn start_test_indexer_impl( db_url: Option, rpc_url: String, reader_writer_config: ReaderWriterConfig, - reset_database: bool, + mut reset_database: bool, data_ingestion_path: Option, cancel: CancellationToken, + new_database: Option<&str>, ) -> (PgIndexerStore, JoinHandle>) { - let db_url = db_url.unwrap_or_else(|| { + let mut db_url = db_url.unwrap_or_else(|| { let pg_host = env::var("POSTGRES_HOST").unwrap_or_else(|_| "localhost".into()); let pg_port = env::var("POSTGRES_PORT").unwrap_or_else(|_| "32770".into()); let pw = env::var("POSTGRES_PASSWORD").unwrap_or_else(|_| "postgrespw".into()); format!("postgres://postgres:{pw}@{pg_host}:{pg_port}") }); + if let Some(new_database) = new_database { + db_url = replace_db_name(&db_url, new_database).0; + reset_database = true; + }; + let mut config = IndexerConfig { db_url: Some(db_url.clone().into()), rpc_client_url: rpc_url, diff --git a/crates/iota-indexer/tests/common/mod.rs b/crates/iota-indexer/tests/common/mod.rs index c6950a5e60c..58c5bb84513 100644 --- a/crates/iota-indexer/tests/common/mod.rs +++ b/crates/iota-indexer/tests/common/mod.rs @@ -9,7 +9,10 @@ use std::{ }; use diesel::PgConnection; -use iota_config::node::RunWithRange; +use iota_config::{ + local_ip_utils::{get_available_port, new_local_tcp_socket_for_testing}, + node::RunWithRange, +}; use iota_indexer::{ IndexerConfig, errors::IndexerError, @@ -33,7 +36,8 @@ use tempfile::tempdir; use test_cluster::{TestCluster, TestClusterBuilder}; use tokio::{runtime::Runtime, task::JoinHandle}; -const DEFAULT_DB_URL: &str = "postgres://postgres:postgrespw@localhost:5432/iota_indexer"; +const POSTGRES_URL: &str = "postgres://postgres:postgrespw@localhost:5432"; +const DEFAULT_DB: &str = "iota_indexer"; const DEFAULT_INDEXER_IP: &str = "127.0.0.1"; const DEFAULT_INDEXER_PORT: u16 = 9005; const DEFAULT_SERVER_PORT: u16 = 3000; @@ -53,8 +57,9 @@ impl ApiTestSetup { GLOBAL_API_TEST_SETUP.get_or_init(|| { let runtime = tokio::runtime::Runtime::new().unwrap(); - let (cluster, store, client) = - runtime.block_on(start_test_cluster_with_read_write_indexer(None)); + let (cluster, store, client) = runtime.block_on( + start_test_cluster_with_read_write_indexer(None, Some("shared_test_indexer_db")), + ); Self { runtime, @@ -66,10 +71,50 @@ impl ApiTestSetup { } } +pub struct SimulacrumTestSetup { + pub runtime: Runtime, + pub sim: Arc, + pub store: PgIndexerStore, + /// Indexer RPC Client + pub client: HttpClient, +} + +impl SimulacrumTestSetup { + pub fn get_or_init<'a>( + unique_env_name: &str, + env_initializer: impl Fn(PathBuf) -> Simulacrum, + initialized_env_container: &'a OnceLock, + ) -> &'a SimulacrumTestSetup { + initialized_env_container.get_or_init(|| { + let runtime = tokio::runtime::Runtime::new().unwrap(); + let data_ingestion_path = tempdir().unwrap().into_path(); + + let sim = env_initializer(data_ingestion_path.clone()); + let sim = Arc::new(sim); + + let db_name = format!("simulacrum_env_db_{}", unique_env_name); + let (_, store, _, client) = + runtime.block_on(start_simulacrum_rest_api_with_read_write_indexer( + sim.clone(), + data_ingestion_path, + Some(&db_name), + )); + + SimulacrumTestSetup { + runtime, + sim, + store, + client, + } + }) + } +} + /// Start a [`TestCluster`][`test_cluster::TestCluster`] with a `Read` & /// `Write` indexer pub async fn start_test_cluster_with_read_write_indexer( stop_cluster_after_checkpoint_seq: Option, + database_name: Option<&str>, ) -> (TestCluster, PgIndexerStore, HttpClient) { let temp = tempdir().unwrap().into_path(); let mut builder = TestClusterBuilder::new().with_data_ingestion_dir(temp.clone()); @@ -85,26 +130,32 @@ pub async fn start_test_cluster_with_read_write_indexer( // start indexer in write mode let (pg_store, _pg_store_handle) = start_test_indexer( - Some(DEFAULT_DB_URL.to_owned()), + Some(get_indexer_db_url(None)), cluster.rpc_url().to_string(), ReaderWriterConfig::writer_mode(None), temp.clone(), + database_name, ) .await; // start indexer in read mode - start_indexer_reader(cluster.rpc_url().to_owned(), temp); + let indexer_port = start_indexer_reader(cluster.rpc_url().to_owned(), temp, database_name); // create an RPC client by using the indexer url let rpc_client = HttpClientBuilder::default() - .build(format!( - "http://{DEFAULT_INDEXER_IP}:{DEFAULT_INDEXER_PORT}" - )) + .build(format!("http://{DEFAULT_INDEXER_IP}:{indexer_port}")) .unwrap(); (cluster, pg_store, rpc_client) } +fn get_indexer_db_url(database_name: Option<&str>) -> String { + database_name.map_or_else( + || format!("{POSTGRES_URL}/{DEFAULT_DB}"), + |db_name| format!("{POSTGRES_URL}/{db_name}"), + ) +} + /// Wait for the indexer to catch up to the given checkpoint sequence number /// /// Indexer starts storing data after checkpoint 0 @@ -192,14 +243,20 @@ pub async fn indexer_wait_for_transaction( } /// Start an Indexer instance in `Read` mode -fn start_indexer_reader(fullnode_rpc_url: impl Into, data_ingestion_path: PathBuf) { +fn start_indexer_reader( + fullnode_rpc_url: impl Into, + data_ingestion_path: PathBuf, + database_name: Option<&str>, +) -> u16 { + let db_url = get_indexer_db_url(database_name); + let port = get_available_port(DEFAULT_INDEXER_IP); let config = IndexerConfig { - db_url: Some(DEFAULT_DB_URL.to_owned().into()), + db_url: Some(db_url.clone().into()), rpc_client_url: fullnode_rpc_url.into(), reset_db: true, rpc_server_worker: true, rpc_server_url: DEFAULT_INDEXER_IP.to_owned(), - rpc_server_port: DEFAULT_INDEXER_PORT, + rpc_server_port: port, data_ingestion_path: Some(data_ingestion_path), ..Default::default() }; @@ -207,9 +264,10 @@ fn start_indexer_reader(fullnode_rpc_url: impl Into, data_ingestion_path let registry = prometheus::Registry::default(); init_metrics(®istry); - tokio::spawn(async move { - Indexer::start_reader::(&config, ®istry, DEFAULT_DB_URL.to_owned()).await - }); + tokio::spawn( + async move { Indexer::start_reader::(&config, ®istry, db_url).await }, + ); + port } /// Check if provided error message does match with @@ -228,24 +286,19 @@ pub fn rpc_call_error_msg_matches( }) } -pub fn get_default_fullnode_rpc_api_addr() -> SocketAddr { - format!("127.0.0.1:{}", DEFAULT_SERVER_PORT) - .parse() - .unwrap() -} - /// Set up a test indexer fetching from a REST endpoint served by the given /// Simulacrum. pub async fn start_simulacrum_rest_api_with_write_indexer( sim: Arc, data_ingestion_path: PathBuf, + server_url: Option, + database_name: Option<&str>, ) -> ( JoinHandle<()>, PgIndexerStore, JoinHandle>, ) { - let server_url = get_default_fullnode_rpc_api_addr(); - + let server_url = server_url.unwrap_or_else(new_local_tcp_socket_for_testing); let server_handle = tokio::spawn(async move { iota_rest_api::RestService::new_without_version(sim) .start_service(server_url) @@ -253,10 +306,11 @@ pub async fn start_simulacrum_rest_api_with_write_indexer( }); // Starts indexer let (pg_store, pg_handle) = start_test_indexer( - Some(DEFAULT_DB_URL.to_owned()), + Some(get_indexer_db_url(None)), format!("http://{}", server_url), ReaderWriterConfig::writer_mode(None), data_ingestion_path, + database_name, ) .await; (server_handle, pg_store, pg_handle) @@ -265,24 +319,32 @@ pub async fn start_simulacrum_rest_api_with_write_indexer( pub async fn start_simulacrum_rest_api_with_read_write_indexer( sim: Arc, data_ingestion_path: PathBuf, + database_name: Option<&str>, ) -> ( JoinHandle<()>, PgIndexerStore, JoinHandle>, HttpClient, ) { - let server_url = get_default_fullnode_rpc_api_addr(); - let (server_handle, pg_store, pg_handle) = - start_simulacrum_rest_api_with_write_indexer(sim, data_ingestion_path.clone()).await; + let simulacrum_server_url = new_local_tcp_socket_for_testing(); + let (server_handle, pg_store, pg_handle) = start_simulacrum_rest_api_with_write_indexer( + sim, + data_ingestion_path.clone(), + Some(simulacrum_server_url), + database_name, + ) + .await; // start indexer in read mode - start_indexer_reader(format!("http://{}", server_url), data_ingestion_path); + let indexer_port = start_indexer_reader( + format!("http://{}", simulacrum_server_url), + data_ingestion_path, + database_name, + ); // create an RPC client by using the indexer url let rpc_client = HttpClientBuilder::default() - .build(format!( - "http://{DEFAULT_INDEXER_IP}:{DEFAULT_INDEXER_PORT}" - )) + .build(format!("http://{DEFAULT_INDEXER_IP}:{indexer_port}")) .unwrap(); (server_handle, pg_store, pg_handle, rpc_client) diff --git a/crates/iota-indexer/tests/ingestion_tests.rs b/crates/iota-indexer/tests/ingestion_tests.rs index 0ed00313192..30f115c10f0 100644 --- a/crates/iota-indexer/tests/ingestion_tests.rs +++ b/crates/iota-indexer/tests/ingestion_tests.rs @@ -52,8 +52,13 @@ mod ingestion_tests { // Create a checkpoint which should include the transaction we executed. let checkpoint = sim.create_checkpoint(); - let (_, pg_store, _) = - start_simulacrum_rest_api_with_write_indexer(Arc::new(sim), data_ingestion_path).await; + let (_, pg_store, _) = start_simulacrum_rest_api_with_write_indexer( + Arc::new(sim), + data_ingestion_path, + None, + Some("indexer_ingestion_tests_db"), + ) + .await; indexer_wait_for_checkpoint(&pg_store, 1).await; @@ -97,8 +102,13 @@ mod ingestion_tests { // Create a checkpoint which should include the transaction we executed. let _ = sim.create_checkpoint(); - let (_, pg_store, _) = - start_simulacrum_rest_api_with_write_indexer(Arc::new(sim), data_ingestion_path).await; + let (_, pg_store, _) = start_simulacrum_rest_api_with_write_indexer( + Arc::new(sim), + data_ingestion_path, + None, + Some("indexer_ingestion_tests_db"), + ) + .await; indexer_wait_for_checkpoint(&pg_store, 1).await; diff --git a/crates/iota-indexer/tests/rpc-tests/extended_api.rs b/crates/iota-indexer/tests/rpc-tests/extended_api.rs index 82d5f4ff443..f77c156b3d4 100644 --- a/crates/iota-indexer/tests/rpc-tests/extended_api.rs +++ b/crates/iota-indexer/tests/rpc-tests/extended_api.rs @@ -1,7 +1,7 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use std::{str::FromStr, sync::Arc}; +use std::{str::FromStr, sync::OnceLock}; use iota_json::{call_args, type_args}; use iota_json_rpc_api::{ @@ -18,431 +18,400 @@ use iota_types::{ quorum_driver_types::ExecuteTransactionRequestType, storage::ReadStore, }; -use serial_test::serial; use simulacrum::Simulacrum; use tempfile::tempdir; use test_cluster::TestCluster; -use crate::common::{ - indexer_wait_for_checkpoint, start_simulacrum_rest_api_with_read_write_indexer, - start_test_cluster_with_read_write_indexer, -}; - -#[tokio::test] -#[serial] -async fn get_epochs() { - let data_ingestion_path = tempdir().unwrap().into_path(); - let mut sim = Simulacrum::new(); - sim.set_data_ingestion_path(data_ingestion_path.clone()); - - execute_simulacrum_transactions(&mut sim, 15); - add_checkpoints(&mut sim, 300); - sim.advance_epoch(); - - execute_simulacrum_transactions(&mut sim, 10); - add_checkpoints(&mut sim, 300); - sim.advance_epoch(); - - execute_simulacrum_transactions(&mut sim, 5); - add_checkpoints(&mut sim, 300); +use crate::common::{ApiTestSetup, SimulacrumTestSetup, indexer_wait_for_checkpoint}; - let last_checkpoint = sim.get_latest_checkpoint().unwrap(); +static EXTENDED_API_SHARED_SIMULACRUM_INITIALIZED_ENV: OnceLock = + OnceLock::new(); - let (_, pg_store, _, indexer_client) = - start_simulacrum_rest_api_with_read_write_indexer(Arc::new(sim), data_ingestion_path).await; - indexer_wait_for_checkpoint(&pg_store, last_checkpoint.sequence_number).await; +fn get_or_init_shared_extended_api_simulacrum_env() -> &'static SimulacrumTestSetup { + SimulacrumTestSetup::get_or_init( + "extended_api", + |data_ingestion_path| { + let mut sim = Simulacrum::new(); + sim.set_data_ingestion_path(data_ingestion_path); - let epochs = indexer_client.get_epochs(None, None, None).await.unwrap(); + execute_simulacrum_transactions(&mut sim, 15); + add_checkpoints(&mut sim, 300); + sim.advance_epoch(); - assert_eq!(epochs.data.len(), 3); - assert!(!epochs.has_next_page); + execute_simulacrum_transactions(&mut sim, 10); + add_checkpoints(&mut sim, 300); + sim.advance_epoch(); - let end_of_epoch_info = epochs.data[0].end_of_epoch_info.as_ref().unwrap(); - assert_eq!(epochs.data[0].epoch, 0); - assert_eq!(epochs.data[0].first_checkpoint_id, 0); - assert_eq!(epochs.data[0].epoch_total_transactions, 17); - assert_eq!(end_of_epoch_info.last_checkpoint_id, 301); + execute_simulacrum_transactions(&mut sim, 5); + add_checkpoints(&mut sim, 300); - let end_of_epoch_info = epochs.data[1].end_of_epoch_info.as_ref().unwrap(); - assert_eq!(epochs.data[1].epoch, 1); - assert_eq!(epochs.data[1].first_checkpoint_id, 302); - assert_eq!(epochs.data[1].epoch_total_transactions, 11); - assert_eq!(end_of_epoch_info.last_checkpoint_id, 602); - - assert_eq!(epochs.data[2].epoch, 2); - assert_eq!(epochs.data[2].first_checkpoint_id, 603); - assert_eq!(epochs.data[2].epoch_total_transactions, 0); - assert!(epochs.data[2].end_of_epoch_info.is_none()); + sim + }, + &EXTENDED_API_SHARED_SIMULACRUM_INITIALIZED_ENV, + ) } -#[tokio::test] -#[serial] -async fn get_epochs_descending() { - let data_ingestion_path = tempdir().unwrap().into_path(); - let mut sim = Simulacrum::new(); - sim.set_data_ingestion_path(data_ingestion_path.clone()); - - execute_simulacrum_transactions(&mut sim, 15); - add_checkpoints(&mut sim, 300); - sim.advance_epoch(); - - execute_simulacrum_transactions(&mut sim, 10); - add_checkpoints(&mut sim, 300); - sim.advance_epoch(); - - execute_simulacrum_transactions(&mut sim, 5); - add_checkpoints(&mut sim, 300); - - let last_checkpoint = sim.get_latest_checkpoint().unwrap(); - - let (_, pg_store, _, indexer_client) = - start_simulacrum_rest_api_with_read_write_indexer(Arc::new(sim), data_ingestion_path).await; - indexer_wait_for_checkpoint(&pg_store, last_checkpoint.sequence_number).await; - - let epochs = indexer_client - .get_epochs(None, None, Some(true)) - .await - .unwrap(); - - let actual_epochs_order = epochs - .data - .iter() - .map(|epoch| epoch.epoch) - .collect::>(); - - assert_eq!(epochs.data.len(), 3); - assert!(!epochs.has_next_page); - assert_eq!(actual_epochs_order, [2, 1, 0]) +#[test] +fn get_epochs() { + let SimulacrumTestSetup { + runtime, + sim, + store, + client, + } = get_or_init_shared_extended_api_simulacrum_env(); + + runtime.block_on(async move { + let last_checkpoint = sim.get_latest_checkpoint().unwrap(); + indexer_wait_for_checkpoint(&store, last_checkpoint.sequence_number).await; + + let epochs = client.get_epochs(None, None, None).await.unwrap(); + + assert_eq!(epochs.data.len(), 3); + assert!(!epochs.has_next_page); + + let end_of_epoch_info = epochs.data[0].end_of_epoch_info.as_ref().unwrap(); + assert_eq!(epochs.data[0].epoch, 0); + assert_eq!(epochs.data[0].first_checkpoint_id, 0); + assert_eq!(epochs.data[0].epoch_total_transactions, 17); + assert_eq!(end_of_epoch_info.last_checkpoint_id, 301); + + let end_of_epoch_info = epochs.data[1].end_of_epoch_info.as_ref().unwrap(); + assert_eq!(epochs.data[1].epoch, 1); + assert_eq!(epochs.data[1].first_checkpoint_id, 302); + assert_eq!(epochs.data[1].epoch_total_transactions, 11); + assert_eq!(end_of_epoch_info.last_checkpoint_id, 602); + + assert_eq!(epochs.data[2].epoch, 2); + assert_eq!(epochs.data[2].first_checkpoint_id, 603); + assert_eq!(epochs.data[2].epoch_total_transactions, 0); + assert!(epochs.data[2].end_of_epoch_info.is_none()); + }); } -#[tokio::test] -#[serial] -async fn get_epochs_paging() { - let data_ingestion_path = tempdir().unwrap().into_path(); - let mut sim = Simulacrum::new(); - sim.set_data_ingestion_path(data_ingestion_path.clone()); - - execute_simulacrum_transactions(&mut sim, 15); - add_checkpoints(&mut sim, 300); - sim.advance_epoch(); - - execute_simulacrum_transactions(&mut sim, 10); - add_checkpoints(&mut sim, 300); - sim.advance_epoch(); - - execute_simulacrum_transactions(&mut sim, 5); - add_checkpoints(&mut sim, 300); - - let last_checkpoint = sim.get_latest_checkpoint().unwrap(); - - let (_, pg_store, _, indexer_client) = - start_simulacrum_rest_api_with_read_write_indexer(Arc::new(sim), data_ingestion_path).await; - indexer_wait_for_checkpoint(&pg_store, last_checkpoint.sequence_number).await; - - let epochs = indexer_client - .get_epochs(None, Some(2), None) - .await - .unwrap(); - let actual_epochs_order = epochs - .data - .iter() - .map(|epoch| epoch.epoch) - .collect::>(); - - assert_eq!(epochs.data.len(), 2); - assert!(epochs.has_next_page); - assert_eq!(epochs.next_cursor, Some(1.into())); - assert_eq!(actual_epochs_order, [0, 1]); - - let epochs = indexer_client - .get_epochs(Some(1.into()), Some(2), None) - .await - .unwrap(); - let actual_epochs_order = epochs - .data - .iter() - .map(|epoch| epoch.epoch) - .collect::>(); - - assert_eq!(epochs.data.len(), 1); - assert!(!epochs.has_next_page); - assert_eq!(epochs.next_cursor, Some(2.into())); - assert_eq!(actual_epochs_order, [2]); +#[test] +fn get_epochs_descending() { + let SimulacrumTestSetup { + runtime, + sim, + store, + client, + } = get_or_init_shared_extended_api_simulacrum_env(); + + runtime.block_on(async move { + let last_checkpoint = sim.get_latest_checkpoint().unwrap(); + indexer_wait_for_checkpoint(&store, last_checkpoint.sequence_number).await; + + let epochs = client.get_epochs(None, None, Some(true)).await.unwrap(); + + let actual_epochs_order = epochs + .data + .iter() + .map(|epoch| epoch.epoch) + .collect::>(); + + assert_eq!(epochs.data.len(), 3); + assert!(!epochs.has_next_page); + assert_eq!(actual_epochs_order, [2, 1, 0]) + }); } -#[tokio::test] -#[serial] -async fn get_epoch_metrics() { - let data_ingestion_path = tempdir().unwrap().into_path(); - let mut sim = Simulacrum::new(); - sim.set_data_ingestion_path(data_ingestion_path.clone()); - - execute_simulacrum_transactions(&mut sim, 15); - add_checkpoints(&mut sim, 300); - sim.advance_epoch(); - - execute_simulacrum_transactions(&mut sim, 10); - add_checkpoints(&mut sim, 300); - sim.advance_epoch(); - - execute_simulacrum_transactions(&mut sim, 5); - add_checkpoints(&mut sim, 300); - - let last_checkpoint = sim.get_latest_checkpoint().unwrap(); - - let (_, pg_store, _, indexer_client) = - start_simulacrum_rest_api_with_read_write_indexer(Arc::new(sim), data_ingestion_path).await; - indexer_wait_for_checkpoint(&pg_store, last_checkpoint.sequence_number).await; - - let epoch_metrics = indexer_client - .get_epoch_metrics(None, None, None) - .await - .unwrap(); - - assert_eq!(epoch_metrics.data.len(), 3); - assert!(!epoch_metrics.has_next_page); - - let end_of_epoch_info = epoch_metrics.data[0].end_of_epoch_info.as_ref().unwrap(); - assert_eq!(epoch_metrics.data[0].epoch, 0); - assert_eq!(epoch_metrics.data[0].first_checkpoint_id, 0); - assert_eq!(epoch_metrics.data[0].epoch_total_transactions, 17); - assert_eq!(end_of_epoch_info.last_checkpoint_id, 301); - - let end_of_epoch_info = epoch_metrics.data[1].end_of_epoch_info.as_ref().unwrap(); - assert_eq!(epoch_metrics.data[1].epoch, 1); - assert_eq!(epoch_metrics.data[1].first_checkpoint_id, 302); - assert_eq!(epoch_metrics.data[1].epoch_total_transactions, 11); - assert_eq!(end_of_epoch_info.last_checkpoint_id, 602); - - assert_eq!(epoch_metrics.data[2].epoch, 2); - assert_eq!(epoch_metrics.data[2].first_checkpoint_id, 603); - assert_eq!(epoch_metrics.data[2].epoch_total_transactions, 0); - assert!(epoch_metrics.data[2].end_of_epoch_info.is_none()); +#[test] +fn get_epochs_paging() { + let SimulacrumTestSetup { + runtime, + sim, + store, + client, + } = get_or_init_shared_extended_api_simulacrum_env(); + + runtime.block_on(async move { + let last_checkpoint = sim.get_latest_checkpoint().unwrap(); + indexer_wait_for_checkpoint(&store, last_checkpoint.sequence_number).await; + + let epochs = client.get_epochs(None, Some(2), None).await.unwrap(); + let actual_epochs_order = epochs + .data + .iter() + .map(|epoch| epoch.epoch) + .collect::>(); + + assert_eq!(epochs.data.len(), 2); + assert!(epochs.has_next_page); + assert_eq!(epochs.next_cursor, Some(1.into())); + assert_eq!(actual_epochs_order, [0, 1]); + + let epochs = client + .get_epochs(Some(1.into()), Some(2), None) + .await + .unwrap(); + let actual_epochs_order = epochs + .data + .iter() + .map(|epoch| epoch.epoch) + .collect::>(); + + assert_eq!(epochs.data.len(), 1); + assert!(!epochs.has_next_page); + assert_eq!(epochs.next_cursor, Some(2.into())); + assert_eq!(actual_epochs_order, [2]); + }); } -#[tokio::test] -#[serial] -async fn get_epoch_metrics_descending() { - let data_ingestion_path = tempdir().unwrap().into_path(); - let mut sim = Simulacrum::new(); - sim.set_data_ingestion_path(data_ingestion_path.clone()); - - execute_simulacrum_transactions(&mut sim, 15); - add_checkpoints(&mut sim, 300); - sim.advance_epoch(); - - execute_simulacrum_transactions(&mut sim, 10); - add_checkpoints(&mut sim, 300); - sim.advance_epoch(); - - execute_simulacrum_transactions(&mut sim, 5); - add_checkpoints(&mut sim, 300); - - let last_checkpoint = sim.get_latest_checkpoint().unwrap(); - - let (_, pg_store, _, indexer_client) = - start_simulacrum_rest_api_with_read_write_indexer(Arc::new(sim), data_ingestion_path).await; - indexer_wait_for_checkpoint(&pg_store, last_checkpoint.sequence_number).await; - - let epochs = indexer_client - .get_epoch_metrics(None, None, Some(true)) - .await - .unwrap(); - - let actual_epochs_order = epochs - .data - .iter() - .map(|epoch| epoch.epoch) - .collect::>(); - - assert_eq!(epochs.data.len(), 3); - assert!(!epochs.has_next_page); - assert_eq!(actual_epochs_order, [2, 1, 0]) +#[test] +fn get_epoch_metrics() { + let SimulacrumTestSetup { + runtime, + sim, + store, + client, + } = get_or_init_shared_extended_api_simulacrum_env(); + + runtime.block_on(async move { + let last_checkpoint = sim.get_latest_checkpoint().unwrap(); + indexer_wait_for_checkpoint(&store, last_checkpoint.sequence_number).await; + + let epoch_metrics = client.get_epoch_metrics(None, None, None).await.unwrap(); + + assert_eq!(epoch_metrics.data.len(), 3); + assert!(!epoch_metrics.has_next_page); + + let end_of_epoch_info = epoch_metrics.data[0].end_of_epoch_info.as_ref().unwrap(); + assert_eq!(epoch_metrics.data[0].epoch, 0); + assert_eq!(epoch_metrics.data[0].first_checkpoint_id, 0); + assert_eq!(epoch_metrics.data[0].epoch_total_transactions, 17); + assert_eq!(end_of_epoch_info.last_checkpoint_id, 301); + + let end_of_epoch_info = epoch_metrics.data[1].end_of_epoch_info.as_ref().unwrap(); + assert_eq!(epoch_metrics.data[1].epoch, 1); + assert_eq!(epoch_metrics.data[1].first_checkpoint_id, 302); + assert_eq!(epoch_metrics.data[1].epoch_total_transactions, 11); + assert_eq!(end_of_epoch_info.last_checkpoint_id, 602); + + assert_eq!(epoch_metrics.data[2].epoch, 2); + assert_eq!(epoch_metrics.data[2].first_checkpoint_id, 603); + assert_eq!(epoch_metrics.data[2].epoch_total_transactions, 0); + assert!(epoch_metrics.data[2].end_of_epoch_info.is_none()); + }); } -#[tokio::test] -#[serial] -async fn get_epoch_metrics_paging() { - let data_ingestion_path = tempdir().unwrap().into_path(); - let mut sim = Simulacrum::new(); - sim.set_data_ingestion_path(data_ingestion_path.clone()); - - execute_simulacrum_transactions(&mut sim, 15); - add_checkpoints(&mut sim, 300); - sim.advance_epoch(); - - execute_simulacrum_transactions(&mut sim, 10); - add_checkpoints(&mut sim, 300); - sim.advance_epoch(); - - execute_simulacrum_transactions(&mut sim, 5); - add_checkpoints(&mut sim, 300); - - let last_checkpoint = sim.get_latest_checkpoint().unwrap(); - - let (_, pg_store, _, indexer_client) = - start_simulacrum_rest_api_with_read_write_indexer(Arc::new(sim), data_ingestion_path).await; - indexer_wait_for_checkpoint(&pg_store, last_checkpoint.sequence_number).await; - - let epochs = indexer_client - .get_epoch_metrics(None, Some(2), None) - .await - .unwrap(); - let actual_epochs_order = epochs - .data - .iter() - .map(|epoch| epoch.epoch) - .collect::>(); - - assert_eq!(epochs.data.len(), 2); - assert!(epochs.has_next_page); - assert_eq!(epochs.next_cursor, Some(1.into())); - assert_eq!(actual_epochs_order, [0, 1]); - - let epochs = indexer_client - .get_epoch_metrics(Some(1.into()), Some(2), None) - .await - .unwrap(); - let actual_epochs_order = epochs - .data - .iter() - .map(|epoch| epoch.epoch) - .collect::>(); - - assert_eq!(epochs.data.len(), 1); - assert!(!epochs.has_next_page); - assert_eq!(epochs.next_cursor, Some(2.into())); - assert_eq!(actual_epochs_order, [2]); +#[test] +fn get_epoch_metrics_descending() { + let SimulacrumTestSetup { + runtime, + sim, + store, + client, + } = get_or_init_shared_extended_api_simulacrum_env(); + + runtime.block_on(async move { + let last_checkpoint = sim.get_latest_checkpoint().unwrap(); + indexer_wait_for_checkpoint(&store, last_checkpoint.sequence_number).await; + + let epochs = client + .get_epoch_metrics(None, None, Some(true)) + .await + .unwrap(); + + let actual_epochs_order = epochs + .data + .iter() + .map(|epoch| epoch.epoch) + .collect::>(); + + assert_eq!(epochs.data.len(), 3); + assert!(!epochs.has_next_page); + assert_eq!(actual_epochs_order, [2, 1, 0]); + }); } -#[tokio::test] -#[serial] -async fn get_current_epoch() { - let data_ingestion_path = tempdir().unwrap().into_path(); - let mut sim = Simulacrum::new(); - sim.set_data_ingestion_path(data_ingestion_path.clone()); - - execute_simulacrum_transactions(&mut sim, 15); - add_checkpoints(&mut sim, 300); - sim.advance_epoch(); - - execute_simulacrum_transactions(&mut sim, 10); - add_checkpoints(&mut sim, 300); - sim.advance_epoch(); - - execute_simulacrum_transactions(&mut sim, 5); - add_checkpoints(&mut sim, 300); - - let last_checkpoint = sim.get_latest_checkpoint().unwrap(); - - let (_, pg_store, _, indexer_client) = - start_simulacrum_rest_api_with_read_write_indexer(Arc::new(sim), data_ingestion_path).await; - indexer_wait_for_checkpoint(&pg_store, last_checkpoint.sequence_number).await; - - let current_epoch = indexer_client.get_current_epoch().await.unwrap(); +#[test] +fn get_epoch_metrics_paging() { + let SimulacrumTestSetup { + runtime, + sim, + store, + client, + } = get_or_init_shared_extended_api_simulacrum_env(); + + runtime.block_on(async move { + let last_checkpoint = sim.get_latest_checkpoint().unwrap(); + indexer_wait_for_checkpoint(&store, last_checkpoint.sequence_number).await; + + let epochs = client.get_epoch_metrics(None, Some(2), None).await.unwrap(); + let actual_epochs_order = epochs + .data + .iter() + .map(|epoch| epoch.epoch) + .collect::>(); + + assert_eq!(epochs.data.len(), 2); + assert!(epochs.has_next_page); + assert_eq!(epochs.next_cursor, Some(1.into())); + assert_eq!(actual_epochs_order, [0, 1]); + + let epochs = client + .get_epoch_metrics(Some(1.into()), Some(2), None) + .await + .unwrap(); + let actual_epochs_order = epochs + .data + .iter() + .map(|epoch| epoch.epoch) + .collect::>(); + + assert_eq!(epochs.data.len(), 1); + assert!(!epochs.has_next_page); + assert_eq!(epochs.next_cursor, Some(2.into())); + assert_eq!(actual_epochs_order, [2]); + }); +} - assert_eq!(current_epoch.epoch, 2); - assert_eq!(current_epoch.first_checkpoint_id, 603); - assert_eq!(current_epoch.epoch_total_transactions, 0); - assert!(current_epoch.end_of_epoch_info.is_none()); +#[test] +fn get_current_epoch() { + let SimulacrumTestSetup { + runtime, + sim, + store, + client, + } = get_or_init_shared_extended_api_simulacrum_env(); + + runtime.block_on(async move { + let last_checkpoint = sim.get_latest_checkpoint().unwrap(); + indexer_wait_for_checkpoint(&store, last_checkpoint.sequence_number).await; + + let current_epoch = client.get_current_epoch().await.unwrap(); + + assert_eq!(current_epoch.epoch, 2); + assert_eq!(current_epoch.first_checkpoint_id, 603); + assert_eq!(current_epoch.epoch_total_transactions, 0); + assert!(current_epoch.end_of_epoch_info.is_none()); + }); } #[ignore = "https://github.com/iotaledger/iota/issues/2197#issuecomment-2371642744"] -#[tokio::test] -#[serial] -async fn get_network_metrics() { - let (_, pg_store, indexer_client) = start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 10).await; - - let network_metrics = indexer_client.get_network_metrics().await.unwrap(); - - println!("{:#?}", network_metrics); +#[test] +fn get_network_metrics() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(&store, 10).await; + + let network_metrics = client.get_network_metrics().await.unwrap(); + + println!("{:#?}", network_metrics); + }); } #[ignore = "https://github.com/iotaledger/iota/issues/2197#issuecomment-2371642744"] -#[tokio::test] -#[serial] -async fn get_move_call_metrics() { - let (cluster, pg_store, indexer_client) = - start_test_cluster_with_read_write_indexer(None).await; - - execute_move_fn(&cluster).await.unwrap(); - - let latest_checkpoint_sn = cluster - .rpc_client() - .get_latest_checkpoint_sequence_number() - .await - .unwrap(); - indexer_wait_for_checkpoint(&pg_store, latest_checkpoint_sn.into_inner()).await; - - let move_call_metrics = indexer_client.get_move_call_metrics().await.unwrap(); - - // TODO: Why is the move call not included in the stats? - assert_eq!(move_call_metrics.rank_3_days.len(), 0); - assert_eq!(move_call_metrics.rank_7_days.len(), 0); - assert_eq!(move_call_metrics.rank_30_days.len(), 0); +#[test] +fn get_move_call_metrics() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + .. + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + execute_move_fn(&cluster).await.unwrap(); + + let latest_checkpoint_sn = cluster + .rpc_client() + .get_latest_checkpoint_sequence_number() + .await + .unwrap(); + indexer_wait_for_checkpoint(&store, latest_checkpoint_sn.into_inner()).await; + + let move_call_metrics = client.get_move_call_metrics().await.unwrap(); + + // TODO: Why is the move call not included in the stats? + assert_eq!(move_call_metrics.rank_3_days.len(), 0); + assert_eq!(move_call_metrics.rank_7_days.len(), 0); + assert_eq!(move_call_metrics.rank_30_days.len(), 0); + }); } #[ignore = "https://github.com/iotaledger/iota/issues/2197#issuecomment-2371642744"] -#[tokio::test] -#[serial] -async fn get_latest_address_metrics() { - let (_, pg_store, indexer_client) = start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 10).await; - - let address_metrics = indexer_client.get_latest_address_metrics().await.unwrap(); - - println!("{:#?}", address_metrics); +#[test] +fn get_latest_address_metrics() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(&store, 10).await; + + let address_metrics = client.get_latest_address_metrics().await.unwrap(); + + println!("{:#?}", address_metrics); + }); } #[ignore = "https://github.com/iotaledger/iota/issues/2197#issuecomment-2371642744"] -#[tokio::test] -#[serial] -async fn get_checkpoint_address_metrics() { - let (_, pg_store, indexer_client) = start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 10).await; - - let address_metrics = indexer_client - .get_checkpoint_address_metrics(0) - .await - .unwrap(); - - println!("{:#?}", address_metrics); +#[test] +fn get_checkpoint_address_metrics() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(&store, 10).await; + + let address_metrics = client.get_checkpoint_address_metrics(0).await.unwrap(); + + println!("{:#?}", address_metrics); + }); } #[ignore = "https://github.com/iotaledger/iota/issues/2197#issuecomment-2371642744"] -#[tokio::test] -#[serial] -async fn get_all_epoch_address_metrics() { - let (_, pg_store, indexer_client) = start_test_cluster_with_read_write_indexer(None).await; - indexer_wait_for_checkpoint(&pg_store, 10).await; - - let address_metrics = indexer_client - .get_all_epoch_address_metrics(None) - .await - .unwrap(); - - println!("{:#?}", address_metrics); +#[test] +fn get_all_epoch_address_metrics() { + let ApiTestSetup { + runtime, + store, + client, + .. + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + indexer_wait_for_checkpoint(&store, 10).await; + + let address_metrics = client.get_all_epoch_address_metrics(None).await.unwrap(); + + println!("{:#?}", address_metrics); + }); } -#[tokio::test] -#[serial] -async fn get_total_transactions() { - let data_ingestion_path = tempdir().unwrap().into_path(); - let mut sim = Simulacrum::new(); - sim.set_data_ingestion_path(data_ingestion_path.clone()); - execute_simulacrum_transactions(&mut sim, 5); - - let latest_checkpoint = sim.create_checkpoint(); - let total_transactions_count = latest_checkpoint.network_total_transactions; - - let (_, pg_store, _, indexer_client) = - start_simulacrum_rest_api_with_read_write_indexer(Arc::new(sim), data_ingestion_path).await; - indexer_wait_for_checkpoint(&pg_store, latest_checkpoint.sequence_number).await; - - let transactions_cnt = indexer_client.get_total_transactions().await.unwrap(); - assert_eq!(transactions_cnt.into_inner(), total_transactions_count); - assert_eq!(transactions_cnt.into_inner(), 6); +#[test] +fn get_total_transactions() { + let SimulacrumTestSetup { + runtime, + sim, + store, + client, + } = get_or_init_shared_extended_api_simulacrum_env(); + + runtime.block_on(async move { + let latest_checkpoint = sim.get_latest_checkpoint().unwrap(); + let total_transactions_count = latest_checkpoint.network_total_transactions; + indexer_wait_for_checkpoint(&store, latest_checkpoint.sequence_number).await; + + let transactions_cnt = client.get_total_transactions().await.unwrap(); + assert_eq!(transactions_cnt.into_inner(), total_transactions_count); + assert_eq!(transactions_cnt.into_inner(), 33); + }); } async fn execute_move_fn(cluster: &TestCluster) -> Result<(), anyhow::Error> { diff --git a/crates/iota-indexer/tests/rpc-tests/main.rs b/crates/iota-indexer/tests/rpc-tests/main.rs index 5c7dd6490c7..bd1a3788c89 100644 --- a/crates/iota-indexer/tests/rpc-tests/main.rs +++ b/crates/iota-indexer/tests/rpc-tests/main.rs @@ -4,7 +4,7 @@ #[allow(dead_code)] #[path = "../common/mod.rs"] mod common; -#[cfg(feature = "pg_integration")] +#[cfg(feature = "shared_test_runtime")] mod extended_api; #[cfg(feature = "shared_test_runtime")] mod governance_api; From 0682515441710e50337a152a949cd8d5d0081400 Mon Sep 17 00:00:00 2001 From: tomxey Date: Wed, 30 Oct 2024 12:02:32 +0100 Subject: [PATCH 12/24] test(iota-indexer): Add tests for `CoinApi` (#3580) * feat(iota-indexer): Add tests for CoinApi * Simplify `once_prepare_addr_with_iota_and_custom_coins`, reorder modules in main.rs * Fixes after rebase * Change return type of `once_prepare_addr_with_iota_and_custom_coins` * Rename helper test functions * Fix clippy * Fix clippy --- .../data/dummy_modules_publish/Move.toml | 9 + .../sources/trusted_coin.move | 40 ++ .../iota-indexer/tests/rpc-tests/coin_api.rs | 539 ++++++++++++++++++ .../tests/rpc-tests/extended_api.rs | 29 +- crates/iota-indexer/tests/rpc-tests/main.rs | 3 + crates/iota/src/iota_commands.rs | 2 + 6 files changed, 607 insertions(+), 15 deletions(-) create mode 100644 crates/iota-indexer/tests/data/dummy_modules_publish/Move.toml create mode 100644 crates/iota-indexer/tests/data/dummy_modules_publish/sources/trusted_coin.move create mode 100644 crates/iota-indexer/tests/rpc-tests/coin_api.rs diff --git a/crates/iota-indexer/tests/data/dummy_modules_publish/Move.toml b/crates/iota-indexer/tests/data/dummy_modules_publish/Move.toml new file mode 100644 index 00000000000..e518fc169a9 --- /dev/null +++ b/crates/iota-indexer/tests/data/dummy_modules_publish/Move.toml @@ -0,0 +1,9 @@ +[package] +name = "Examples" +version = "0.0.1" + +[dependencies] +Iota = { local = "../../../../iota-framework/packages/iota-framework" } + +[addresses] +examples = "0x0" diff --git a/crates/iota-indexer/tests/data/dummy_modules_publish/sources/trusted_coin.move b/crates/iota-indexer/tests/data/dummy_modules_publish/sources/trusted_coin.move new file mode 100644 index 00000000000..1a53fb598a5 --- /dev/null +++ b/crates/iota-indexer/tests/data/dummy_modules_publish/sources/trusted_coin.move @@ -0,0 +1,40 @@ +// Copyright (c) Mysten Labs, Inc. +// Modifications Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +/// Example coin with a trusted owner responsible for minting/burning (e.g., a stablecoin) +module examples::trusted_coin { + use std::option; + use iota::coin::{Self, TreasuryCap}; + use iota::transfer; + use iota::tx_context::{Self, TxContext}; + + /// Name of the coin + struct TRUSTED_COIN has drop {} + + /// Register the trusted currency to acquire its `TreasuryCap`. Because + /// this is a module initializer, it ensures the currency only gets + /// registered once. + fun init(witness: TRUSTED_COIN, ctx: &mut TxContext) { + // Get a treasury cap for the coin and give it to the transaction + // sender + let (treasury_cap, metadata) = coin::create_currency(witness, 2, b"TRUSTED", b"Trusted Coin", b"Trusted Coin for test", option::none(), ctx); + transfer::public_freeze_object(metadata); + transfer::public_transfer(treasury_cap, tx_context::sender(ctx)) + } + + public entry fun mint(treasury_cap: &mut TreasuryCap, amount: u64, ctx: &mut TxContext) { + let coin = coin::mint(treasury_cap, amount, ctx); + transfer::public_transfer(coin, tx_context::sender(ctx)); + } + + public entry fun transfer(treasury_cap: TreasuryCap, recipient: address) { + transfer::public_transfer(treasury_cap, recipient); + } + + #[test_only] + /// Wrapper of module initializer for testing + public fun test_init(ctx: &mut TxContext) { + init(TRUSTED_COIN {}, ctx) + } +} diff --git a/crates/iota-indexer/tests/rpc-tests/coin_api.rs b/crates/iota-indexer/tests/rpc-tests/coin_api.rs new file mode 100644 index 00000000000..208d0b166fd --- /dev/null +++ b/crates/iota-indexer/tests/rpc-tests/coin_api.rs @@ -0,0 +1,539 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::{path::PathBuf, str::FromStr}; + +use iota_json::{call_args, type_args}; +use iota_json_rpc_api::{CoinReadApiClient, TransactionBuilderClient, WriteApiClient}; +use iota_json_rpc_types::{ + Balance, CoinPage, IotaCoinMetadata, IotaExecutionStatus, IotaObjectRef, + IotaTransactionBlockEffectsAPI, IotaTransactionBlockResponse, + IotaTransactionBlockResponseOptions, ObjectChange, TransactionBlockBytes, +}; +use iota_move_build::BuildConfig; +use iota_types::{ + IOTA_FRAMEWORK_ADDRESS, + balance::Supply, + base_types::{IotaAddress, ObjectID}, + coin::{COIN_MODULE_NAME, TreasuryCap}, + crypto::{AccountKeyPair, get_key_pair}, + parse_iota_struct_tag, + quorum_driver_types::ExecuteTransactionRequestType, + utils::to_sender_signed_transaction, +}; +use jsonrpsee::http_client::HttpClient; +use test_cluster::TestCluster; +use tokio::sync::OnceCell; + +use crate::common::{ApiTestSetup, indexer_wait_for_object}; + +static COMMON_TESTING_ADDR_AND_CUSTOM_COIN_NAME: OnceCell<(IotaAddress, String)> = + OnceCell::const_new(); + +async fn get_or_init_addr_and_custom_coins( + cluster: &TestCluster, + indexer_client: &HttpClient, +) -> &'static (IotaAddress, String) { + COMMON_TESTING_ADDR_AND_CUSTOM_COIN_NAME + .get_or_init(|| async { + let (address, keypair): (_, AccountKeyPair) = get_key_pair(); + + for _ in 0..5 { + cluster + .fund_address_and_return_gas( + cluster.get_reference_gas_price().await, + Some(500_000_000), + address, + ) + .await; + } + + let (coin_name, coin_object_ref) = + create_and_mint_trusted_coin(cluster, address, keypair, 100_000) + .await + .unwrap(); + + indexer_wait_for_object( + indexer_client, + coin_object_ref.object_id, + coin_object_ref.version, + ) + .await; + + (address, coin_name) + }) + .await +} + +#[test] +fn get_coins_basic_scenario() { + let ApiTestSetup { + runtime, + client, + cluster, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + let (owner, _) = get_or_init_addr_and_custom_coins(cluster, client).await; + + let (result_fullnode, result_indexer) = + get_coins_fullnode_indexer(cluster, client, *owner, None, None, None).await; + + assert!(!result_indexer.data.is_empty()); + assert_eq!(result_fullnode, result_indexer); + }); +} + +#[test] +fn get_coins_with_cursor() { + let ApiTestSetup { + runtime, + client, + cluster, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + let (owner, _) = get_or_init_addr_and_custom_coins(cluster, client).await; + let all_coins = cluster + .rpc_client() + .get_coins(*owner, None, None, None) + .await + .unwrap(); + let cursor = all_coins.data[3].coin_object_id; // get some coin from the middle + + let (result_fullnode, result_indexer) = + get_coins_fullnode_indexer(cluster, client, *owner, None, Some(cursor), None).await; + + assert!(!result_indexer.data.is_empty()); + assert_eq!(result_fullnode, result_indexer); + }); +} + +#[test] +fn get_coins_with_limit() { + let ApiTestSetup { + runtime, + client, + cluster, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + let (owner, _) = get_or_init_addr_and_custom_coins(cluster, client).await; + + let (result_fullnode, result_indexer) = + get_coins_fullnode_indexer(cluster, client, *owner, None, None, Some(2)).await; + + assert!(!result_indexer.data.is_empty()); + assert_eq!(result_fullnode, result_indexer); + }); +} + +#[test] +fn get_coins_custom_coin() { + let ApiTestSetup { + runtime, + client, + cluster, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + let (owner, coin_name) = get_or_init_addr_and_custom_coins(cluster, client).await; + + let (result_fullnode, result_indexer) = get_coins_fullnode_indexer( + cluster, + client, + *owner, + Some(coin_name.clone()), + None, + None, + ) + .await; + + assert_eq!(result_indexer.data.len(), 1); + assert_eq!(result_fullnode, result_indexer); + }); +} + +#[test] +fn get_all_coins_basic_scenario() { + let ApiTestSetup { + runtime, + client, + cluster, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + let (owner, _) = get_or_init_addr_and_custom_coins(cluster, client).await; + + let (result_fullnode, result_indexer) = + get_all_coins_fullnode_indexer(cluster, client, *owner, None, None).await; + + assert!(!result_indexer.data.is_empty()); + assert_eq!(result_fullnode, result_indexer); + }); +} + +#[ignore = "https://github.com/iotaledger/iota/issues/3588"] +#[test] +fn get_all_coins_with_cursor() { + let ApiTestSetup { + runtime, + client, + cluster, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + let (owner, _) = get_or_init_addr_and_custom_coins(cluster, client).await; + + let all_coins = cluster + .rpc_client() + .get_coins(*owner, None, None, None) + .await + .unwrap(); + let cursor = all_coins.data[3].coin_object_id; // get some coin from the middle + + let (result_fullnode_all, result_indexer_all) = + get_all_coins_fullnode_indexer(cluster, client, *owner, None, None).await; + + let (result_fullnode, result_indexer) = + get_all_coins_fullnode_indexer(cluster, client, *owner, Some(cursor), None).await; + + println!("Fullnode all: {:#?}", result_fullnode_all); + println!("Indexer all: {:#?}", result_indexer_all); + println!("Fullnode: {:#?}", result_fullnode); + println!("Indexer: {:#?}", result_indexer); + println!("Cursor: {:#?}", cursor); + + assert!(!result_indexer.data.is_empty()); + assert_eq!(result_fullnode, result_indexer); + }); +} + +#[test] +fn get_all_coins_with_limit() { + let ApiTestSetup { + runtime, + client, + cluster, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + let (owner, _) = get_or_init_addr_and_custom_coins(cluster, client).await; + + let (result_fullnode, result_indexer) = + get_all_coins_fullnode_indexer(cluster, client, *owner, None, Some(2)).await; + + assert!(!result_indexer.data.is_empty()); + assert_eq!(result_fullnode, result_indexer); + }); +} + +#[test] +fn get_balance_iota_coin() { + let ApiTestSetup { + runtime, + client, + cluster, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + let (owner, _) = get_or_init_addr_and_custom_coins(cluster, client).await; + + let (result_fullnode, result_indexer) = + get_balance_fullnode_indexer(cluster, client, *owner, None).await; + + assert_eq!(result_fullnode, result_indexer); + }); +} + +#[test] +fn get_balance_custom_coin() { + let ApiTestSetup { + runtime, + client, + cluster, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + let (owner, coin_name) = get_or_init_addr_and_custom_coins(cluster, client).await; + + let (result_fullnode, result_indexer) = + get_balance_fullnode_indexer(cluster, client, *owner, Some(coin_name.to_string())) + .await; + + assert_eq!(result_fullnode, result_indexer); + }); +} + +#[test] +fn get_all_balances() { + let ApiTestSetup { + runtime, + client, + cluster, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + let (owner, _) = get_or_init_addr_and_custom_coins(cluster, client).await; + + let (mut result_fullnode, mut result_indexer) = + get_all_balances_fullnode_indexer(cluster, client, *owner).await; + + result_fullnode.sort_by_key(|balance: &Balance| balance.coin_type.clone()); + result_indexer.sort_by_key(|balance: &Balance| balance.coin_type.clone()); + + assert_eq!(result_fullnode, result_indexer); + }); +} + +#[test] +fn get_coin_metadata() { + let ApiTestSetup { + runtime, + client, + cluster, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + let (_, coin_name) = get_or_init_addr_and_custom_coins(cluster, client).await; + + let (result_fullnode, result_indexer) = + get_coin_metadata_fullnode_indexer(cluster, client, coin_name.to_string()).await; + + assert!(result_indexer.is_some()); + assert_eq!(result_fullnode, result_indexer); + }); +} + +#[test] +fn get_total_supply() { + let ApiTestSetup { + runtime, + client, + cluster, + .. + } = ApiTestSetup::get_or_init(); + runtime.block_on(async move { + let (_, coin_name) = get_or_init_addr_and_custom_coins(cluster, client).await; + + let (result_fullnode, result_indexer) = + get_total_supply_fullnode_indexer(cluster, client, coin_name.to_string()).await; + + assert_eq!(result_fullnode, result_indexer); + }); +} + +async fn get_coins_fullnode_indexer( + cluster: &TestCluster, + client: &HttpClient, + owner: IotaAddress, + coin_type: Option, + cursor: Option, + limit: Option, +) -> (CoinPage, CoinPage) { + let result_fullnode = cluster + .rpc_client() + .get_coins(owner, coin_type.clone(), cursor, limit) + .await + .unwrap(); + let result_indexer = client + .get_coins(owner, coin_type, cursor, limit) + .await + .unwrap(); + (result_fullnode, result_indexer) +} + +async fn get_all_coins_fullnode_indexer( + cluster: &TestCluster, + client: &HttpClient, + owner: IotaAddress, + cursor: Option, + limit: Option, +) -> (CoinPage, CoinPage) { + let result_fullnode = cluster + .rpc_client() + .get_all_coins(owner, cursor, limit) + .await + .unwrap(); + let result_indexer = client.get_all_coins(owner, cursor, limit).await.unwrap(); + (result_fullnode, result_indexer) +} + +async fn get_balance_fullnode_indexer( + cluster: &TestCluster, + client: &HttpClient, + owner: IotaAddress, + coin_type: Option, +) -> (Balance, Balance) { + let result_fullnode = cluster + .rpc_client() + .get_balance(owner, coin_type.clone()) + .await + .unwrap(); + let result_indexer = client.get_balance(owner, coin_type).await.unwrap(); + (result_fullnode, result_indexer) +} + +async fn get_all_balances_fullnode_indexer( + cluster: &TestCluster, + client: &HttpClient, + owner: IotaAddress, +) -> (Vec, Vec) { + let result_fullnode = cluster.rpc_client().get_all_balances(owner).await.unwrap(); + let result_indexer = client.get_all_balances(owner).await.unwrap(); + (result_fullnode, result_indexer) +} + +async fn get_coin_metadata_fullnode_indexer( + cluster: &TestCluster, + client: &HttpClient, + coin_type: String, +) -> (Option, Option) { + let result_fullnode = cluster + .rpc_client() + .get_coin_metadata(coin_type.clone()) + .await + .unwrap(); + let result_indexer = client.get_coin_metadata(coin_type).await.unwrap(); + (result_fullnode, result_indexer) +} + +async fn get_total_supply_fullnode_indexer( + cluster: &TestCluster, + client: &HttpClient, + coin_type: String, +) -> (Supply, Supply) { + let result_fullnode = cluster + .rpc_client() + .get_total_supply(coin_type.clone()) + .await + .unwrap(); + let result_indexer = client.get_total_supply(coin_type).await.unwrap(); + (result_fullnode, result_indexer) +} + +async fn create_and_mint_trusted_coin( + cluster: &TestCluster, + address: IotaAddress, + account_keypair: AccountKeyPair, + amount: u64, +) -> Result<(String, IotaObjectRef), anyhow::Error> { + let http_client = cluster.rpc_client(); + let coins = http_client + .get_coins(address, None, None, Some(1)) + .await + .unwrap() + .data; + let gas = &coins[0]; + + // Publish test coin package + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.extend(["tests", "data", "dummy_modules_publish"]); + let compiled_package = BuildConfig::default().build(&path).unwrap(); + let with_unpublished_deps = false; + let compiled_modules_bytes = compiled_package.get_package_base64(with_unpublished_deps); + let dependencies = compiled_package.get_dependency_storage_package_ids(); + + let transaction_bytes: TransactionBlockBytes = http_client + .publish( + address, + compiled_modules_bytes, + dependencies, + Some(gas.coin_object_id), + 100_000_000.into(), + ) + .await + .unwrap(); + + let signed_transaction = + to_sender_signed_transaction(transaction_bytes.to_data().unwrap(), &account_keypair); + let (tx_bytes, signatures) = signed_transaction.to_tx_bytes_and_signatures(); + + let tx_response: IotaTransactionBlockResponse = http_client + .execute_transaction_block( + tx_bytes, + signatures, + Some( + IotaTransactionBlockResponseOptions::new() + .with_object_changes() + .with_events(), + ), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await + .unwrap(); + + let object_changes = tx_response.object_changes.as_ref().unwrap(); + let package_id = object_changes + .iter() + .find_map(|change| match change { + ObjectChange::Published { package_id, .. } => Some(package_id), + _ => None, + }) + .unwrap(); + + let coin_name = format!("{package_id}::trusted_coin::TRUSTED_COIN"); + let result: Supply = http_client + .get_total_supply(coin_name.clone()) + .await + .unwrap(); + + assert_eq!(0, result.value); + + let object_changes = tx_response.object_changes.as_ref().unwrap(); + let treasury_cap = object_changes + .iter() + .filter_map(|change| match change { + ObjectChange::Created { + object_id, + object_type, + .. + } => Some((object_id, object_type)), + _ => None, + }) + .find_map(|(object_id, object_type)| { + let coin_type = parse_iota_struct_tag(&coin_name).unwrap(); + (&TreasuryCap::type_(coin_type) == object_type).then_some(object_id) + }) + .unwrap(); + + let transaction_bytes: TransactionBlockBytes = http_client + .move_call( + address, + IOTA_FRAMEWORK_ADDRESS.into(), + COIN_MODULE_NAME.to_string(), + "mint_and_transfer".into(), + type_args![coin_name.clone()].unwrap(), + call_args![treasury_cap, amount, address].unwrap(), + Some(gas.coin_object_id), + 10_000_000.into(), + None, + ) + .await + .unwrap(); + + let signed_transaction = + to_sender_signed_transaction(transaction_bytes.to_data().unwrap(), &account_keypair); + let (tx_bytes, signatures) = signed_transaction.to_tx_bytes_and_signatures(); + + let tx_response = http_client + .execute_transaction_block( + tx_bytes, + signatures, + Some(IotaTransactionBlockResponseOptions::new().with_effects()), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await + .unwrap(); + + let IotaTransactionBlockResponse { effects, .. } = tx_response; + + assert_eq!( + IotaExecutionStatus::Success, + *effects.as_ref().unwrap().status() + ); + + let created_coin_obj_ref = effects.unwrap().created()[0].reference.clone(); + + Ok((coin_name, created_coin_obj_ref)) +} diff --git a/crates/iota-indexer/tests/rpc-tests/extended_api.rs b/crates/iota-indexer/tests/rpc-tests/extended_api.rs index f77c156b3d4..8198a7ef044 100644 --- a/crates/iota-indexer/tests/rpc-tests/extended_api.rs +++ b/crates/iota-indexer/tests/rpc-tests/extended_api.rs @@ -19,7 +19,6 @@ use iota_types::{ storage::ReadStore, }; use simulacrum::Simulacrum; -use tempfile::tempdir; use test_cluster::TestCluster; use crate::common::{ApiTestSetup, SimulacrumTestSetup, indexer_wait_for_checkpoint}; @@ -62,7 +61,7 @@ fn get_epochs() { runtime.block_on(async move { let last_checkpoint = sim.get_latest_checkpoint().unwrap(); - indexer_wait_for_checkpoint(&store, last_checkpoint.sequence_number).await; + indexer_wait_for_checkpoint(store, last_checkpoint.sequence_number).await; let epochs = client.get_epochs(None, None, None).await.unwrap(); @@ -99,7 +98,7 @@ fn get_epochs_descending() { runtime.block_on(async move { let last_checkpoint = sim.get_latest_checkpoint().unwrap(); - indexer_wait_for_checkpoint(&store, last_checkpoint.sequence_number).await; + indexer_wait_for_checkpoint(store, last_checkpoint.sequence_number).await; let epochs = client.get_epochs(None, None, Some(true)).await.unwrap(); @@ -126,7 +125,7 @@ fn get_epochs_paging() { runtime.block_on(async move { let last_checkpoint = sim.get_latest_checkpoint().unwrap(); - indexer_wait_for_checkpoint(&store, last_checkpoint.sequence_number).await; + indexer_wait_for_checkpoint(store, last_checkpoint.sequence_number).await; let epochs = client.get_epochs(None, Some(2), None).await.unwrap(); let actual_epochs_order = epochs @@ -168,7 +167,7 @@ fn get_epoch_metrics() { runtime.block_on(async move { let last_checkpoint = sim.get_latest_checkpoint().unwrap(); - indexer_wait_for_checkpoint(&store, last_checkpoint.sequence_number).await; + indexer_wait_for_checkpoint(store, last_checkpoint.sequence_number).await; let epoch_metrics = client.get_epoch_metrics(None, None, None).await.unwrap(); @@ -205,7 +204,7 @@ fn get_epoch_metrics_descending() { runtime.block_on(async move { let last_checkpoint = sim.get_latest_checkpoint().unwrap(); - indexer_wait_for_checkpoint(&store, last_checkpoint.sequence_number).await; + indexer_wait_for_checkpoint(store, last_checkpoint.sequence_number).await; let epochs = client .get_epoch_metrics(None, None, Some(true)) @@ -235,7 +234,7 @@ fn get_epoch_metrics_paging() { runtime.block_on(async move { let last_checkpoint = sim.get_latest_checkpoint().unwrap(); - indexer_wait_for_checkpoint(&store, last_checkpoint.sequence_number).await; + indexer_wait_for_checkpoint(store, last_checkpoint.sequence_number).await; let epochs = client.get_epoch_metrics(None, Some(2), None).await.unwrap(); let actual_epochs_order = epochs @@ -277,7 +276,7 @@ fn get_current_epoch() { runtime.block_on(async move { let last_checkpoint = sim.get_latest_checkpoint().unwrap(); - indexer_wait_for_checkpoint(&store, last_checkpoint.sequence_number).await; + indexer_wait_for_checkpoint(store, last_checkpoint.sequence_number).await; let current_epoch = client.get_current_epoch().await.unwrap(); @@ -299,7 +298,7 @@ fn get_network_metrics() { } = ApiTestSetup::get_or_init(); runtime.block_on(async move { - indexer_wait_for_checkpoint(&store, 10).await; + indexer_wait_for_checkpoint(store, 10).await; let network_metrics = client.get_network_metrics().await.unwrap(); @@ -319,14 +318,14 @@ fn get_move_call_metrics() { } = ApiTestSetup::get_or_init(); runtime.block_on(async move { - execute_move_fn(&cluster).await.unwrap(); + execute_move_fn(cluster).await.unwrap(); let latest_checkpoint_sn = cluster .rpc_client() .get_latest_checkpoint_sequence_number() .await .unwrap(); - indexer_wait_for_checkpoint(&store, latest_checkpoint_sn.into_inner()).await; + indexer_wait_for_checkpoint(store, latest_checkpoint_sn.into_inner()).await; let move_call_metrics = client.get_move_call_metrics().await.unwrap(); @@ -348,7 +347,7 @@ fn get_latest_address_metrics() { } = ApiTestSetup::get_or_init(); runtime.block_on(async move { - indexer_wait_for_checkpoint(&store, 10).await; + indexer_wait_for_checkpoint(store, 10).await; let address_metrics = client.get_latest_address_metrics().await.unwrap(); @@ -367,7 +366,7 @@ fn get_checkpoint_address_metrics() { } = ApiTestSetup::get_or_init(); runtime.block_on(async move { - indexer_wait_for_checkpoint(&store, 10).await; + indexer_wait_for_checkpoint(store, 10).await; let address_metrics = client.get_checkpoint_address_metrics(0).await.unwrap(); @@ -386,7 +385,7 @@ fn get_all_epoch_address_metrics() { } = ApiTestSetup::get_or_init(); runtime.block_on(async move { - indexer_wait_for_checkpoint(&store, 10).await; + indexer_wait_for_checkpoint(store, 10).await; let address_metrics = client.get_all_epoch_address_metrics(None).await.unwrap(); @@ -406,7 +405,7 @@ fn get_total_transactions() { runtime.block_on(async move { let latest_checkpoint = sim.get_latest_checkpoint().unwrap(); let total_transactions_count = latest_checkpoint.network_total_transactions; - indexer_wait_for_checkpoint(&store, latest_checkpoint.sequence_number).await; + indexer_wait_for_checkpoint(store, latest_checkpoint.sequence_number).await; let transactions_cnt = client.get_total_transactions().await.unwrap(); assert_eq!(transactions_cnt.into_inner(), total_transactions_count); diff --git a/crates/iota-indexer/tests/rpc-tests/main.rs b/crates/iota-indexer/tests/rpc-tests/main.rs index bd1a3788c89..3c95735c5ad 100644 --- a/crates/iota-indexer/tests/rpc-tests/main.rs +++ b/crates/iota-indexer/tests/rpc-tests/main.rs @@ -1,6 +1,9 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +#[cfg(feature = "shared_test_runtime")] +mod coin_api; + #[allow(dead_code)] #[path = "../common/mod.rs"] mod common; diff --git a/crates/iota/src/iota_commands.rs b/crates/iota/src/iota_commands.rs index 002478a8aa4..ea33ca93d87 100644 --- a/crates/iota/src/iota_commands.rs +++ b/crates/iota/src/iota_commands.rs @@ -757,6 +757,7 @@ async fn start( fullnode_url.clone(), ReaderWriterConfig::writer_mode(None), data_ingestion_path.clone(), + None, ) .await; info!("Indexer in writer mode started"); @@ -767,6 +768,7 @@ async fn start( fullnode_url.clone(), ReaderWriterConfig::reader_mode(indexer_address.to_string()), data_ingestion_path, + None, ) .await; info!("Indexer in reader mode started"); From 8a267a27747a1dd661f609f6de8dfdb996df66d2 Mon Sep 17 00:00:00 2001 From: Samuel Rufinatscha Date: Thu, 31 Oct 2024 09:15:36 +0100 Subject: [PATCH 13/24] refactor: Remove global `exchange_rate` and `validators_apys_` caches in order to work with `cargo test` (#3774) --- crates/iota-indexer/README.md | 2 +- .../iota-indexer/src/apis/governance_api.rs | 77 +++++++++--- crates/iota-indexer/tests/common/mod.rs | 15 ++- .../tests/rpc-tests/governance_api.rs | 115 +++++++++--------- 4 files changed, 125 insertions(+), 84 deletions(-) diff --git a/crates/iota-indexer/README.md b/crates/iota-indexer/README.md index f19bead54e7..ffdcd0672a9 100644 --- a/crates/iota-indexer/README.md +++ b/crates/iota-indexer/README.md @@ -104,7 +104,7 @@ The crate provides following tests currently: # run tests requiring only postgres integration cargo nextest run --features pg_integration --test-threads 1 # run rpc tests with shared runtime -cargo test --features shared_test_runtime -- --test-threads 1 +cargo test --features shared_test_runtime ``` For a better testing experience is possible to use [nextest](https://nexte.st/) diff --git a/crates/iota-indexer/src/apis/governance_api.rs b/crates/iota-indexer/src/apis/governance_api.rs index d697288cfdb..57e42103b26 100644 --- a/crates/iota-indexer/src/apis/governance_api.rs +++ b/crates/iota-indexer/src/apis/governance_api.rs @@ -2,10 +2,10 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use std::collections::BTreeMap; +use std::{collections::BTreeMap, sync::Arc}; use async_trait::async_trait; -use cached::{SizedCache, proc_macro::cached}; +use cached::{Cached, SizedCache}; use diesel::r2d2::R2D2Connection; use iota_json_rpc::{IotaRpcModule, governance_api::ValidatorExchangeRates}; use iota_json_rpc_api::GovernanceReadApiServer; @@ -23,6 +23,7 @@ use iota_types::{ timelock::timelocked_staked_iota::TimelockedStakedIota, }; use jsonrpsee::{RpcModule, core::RpcResult}; +use tokio::sync::Mutex; use crate::{errors::IndexerError, indexer_reader::IndexerReader}; @@ -32,11 +33,17 @@ const MAX_QUERY_STAKED_OBJECTS: usize = 1000; #[derive(Clone)] pub struct GovernanceReadApi { inner: IndexerReader, + exchange_rates_cache: Arc>>>, + validators_apys_cache: Arc>>>, } impl GovernanceReadApi { pub fn new(inner: IndexerReader) -> Self { - Self { inner } + Self { + inner, + exchange_rates_cache: Arc::new(Mutex::new(SizedCache::with_size(1))), + validators_apys_cache: Arc::new(Mutex::new(SizedCache::with_size(1))), + } } /// Get a validator's APY by its address @@ -44,7 +51,9 @@ impl GovernanceReadApi { &self, address: &IotaAddress, ) -> Result, IndexerError> { - let apys = validators_apys_map(self.get_validators_apy().await?); + let apys = self + .validators_apys_map(self.get_validators_apy().await?) + .await; Ok(apys.get(address).copied()) } @@ -261,6 +270,28 @@ impl GovernanceReadApi { } Ok(delegated_stakes) } + + /// Cache a map representing the validators' APYs for this epoch + async fn validators_apys_map(&self, apys: ValidatorApys) -> BTreeMap { + // check if the apys are already in the cache + if let Some(cached_apys) = self + .validators_apys_cache + .lock() + .await + .cache_get(&apys.epoch) + { + return cached_apys.clone(); + } + + let ret = BTreeMap::from_iter(apys.apys.iter().map(|x| (x.address, x.apy))); + // insert the apys into the cache + self.validators_apys_cache + .lock() + .await + .cache_set(apys.epoch, ret.clone()); + + ret + } } fn stake_status( @@ -292,15 +323,31 @@ fn stake_status( /// Cached exchange rates for validators for the given epoch, the cache size is /// 1, it will be cleared when the epoch changes. rates are in descending order /// by epoch. -#[cached( - type = "SizedCache>", - create = "{ SizedCache::with_size(1) }", - convert = "{ system_state_summary.epoch }", - result = true -)] pub async fn exchange_rates( state: &GovernanceReadApi, system_state_summary: &IotaSystemStateSummary, +) -> Result, IndexerError> { + let epoch = system_state_summary.epoch; + + let mut cache = state.exchange_rates_cache.lock().await; + + // Check if the exchange rates for the current epoch are cached + if let Some(cached_rates) = cache.cache_get(&epoch) { + return Ok(cached_rates.clone()); + } + + // Cache miss: compute exchange rates + let exchange_rates = compute_exchange_rates(state, system_state_summary).await?; + + // Store in cache + cache.cache_set(epoch, exchange_rates.clone()); + + Ok(exchange_rates) +} + +pub async fn compute_exchange_rates( + state: &GovernanceReadApi, + system_state_summary: &IotaSystemStateSummary, ) -> Result, IndexerError> { // Get validator rate tables let mut tables = vec![]; @@ -384,16 +431,6 @@ pub async fn exchange_rates( Ok(exchange_rates) } -/// Cache a map representing the validators' APYs for this epoch -#[cached( - type = "SizedCache>", - create = "{ SizedCache::with_size(1) }", - convert = " {apys.epoch} " -)] -fn validators_apys_map(apys: ValidatorApys) -> BTreeMap { - BTreeMap::from_iter(apys.apys.iter().map(|x| (x.address, x.apy))) -} - #[async_trait] impl GovernanceReadApiServer for GovernanceReadApi { async fn get_stakes_by_ids( diff --git a/crates/iota-indexer/tests/common/mod.rs b/crates/iota-indexer/tests/common/mod.rs index 58c5bb84513..8f97d1654aa 100644 --- a/crates/iota-indexer/tests/common/mod.rs +++ b/crates/iota-indexer/tests/common/mod.rs @@ -26,6 +26,7 @@ use iota_metrics::init_metrics; use iota_types::{ base_types::{ObjectID, SequenceNumber}, digests::TransactionDigest, + object::Object, }; use jsonrpsee::{ http_client::{HttpClient, HttpClientBuilder}, @@ -57,9 +58,12 @@ impl ApiTestSetup { GLOBAL_API_TEST_SETUP.get_or_init(|| { let runtime = tokio::runtime::Runtime::new().unwrap(); - let (cluster, store, client) = runtime.block_on( - start_test_cluster_with_read_write_indexer(None, Some("shared_test_indexer_db")), - ); + let (cluster, store, client) = + runtime.block_on(start_test_cluster_with_read_write_indexer( + None, + Some("shared_test_indexer_db"), + None, + )); Self { runtime, @@ -115,6 +119,7 @@ impl SimulacrumTestSetup { pub async fn start_test_cluster_with_read_write_indexer( stop_cluster_after_checkpoint_seq: Option, database_name: Option<&str>, + objects: Option>, ) -> (TestCluster, PgIndexerStore, HttpClient) { let temp = tempdir().unwrap().into_path(); let mut builder = TestClusterBuilder::new().with_data_ingestion_dir(temp.clone()); @@ -126,6 +131,10 @@ pub async fn start_test_cluster_with_read_write_indexer( ))); }; + if let Some(objects) = objects { + builder = builder.with_objects(objects); + } + let cluster = builder.build().await; // start indexer in write mode diff --git a/crates/iota-indexer/tests/rpc-tests/governance_api.rs b/crates/iota-indexer/tests/rpc-tests/governance_api.rs index c59c6637e49..d2ff07927e3 100644 --- a/crates/iota-indexer/tests/rpc-tests/governance_api.rs +++ b/crates/iota-indexer/tests/rpc-tests/governance_api.rs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use iota_json_rpc_api::{GovernanceReadApiClient, TransactionBuilderClient}; -use iota_json_rpc_types::{DelegatedStake, StakeStatus, TransactionBlockBytes}; +use iota_json_rpc_types::{ + DelegatedStake, DelegatedTimelockedStake, StakeStatus, TransactionBlockBytes, +}; use iota_test_transaction_builder::TestTransactionBuilder; use iota_types::{ IOTA_FRAMEWORK_ADDRESS, IOTA_SYSTEM_ADDRESS, @@ -19,18 +21,17 @@ use move_core_types::{identifier::Identifier, language_storage::TypeTag}; use crate::common::{ ApiTestSetup, indexer_wait_for_checkpoint, indexer_wait_for_latest_checkpoint, indexer_wait_for_object, indexer_wait_for_transaction, + start_test_cluster_with_read_write_indexer, }; #[test] fn test_staking() { - let ApiTestSetup { - runtime, - store, - client, - cluster, - } = ApiTestSetup::get_or_init(); + let ApiTestSetup { runtime, .. } = ApiTestSetup::get_or_init(); runtime.block_on(async move { + let (cluster, store, client) = + &start_test_cluster_with_read_write_indexer(None, Some("test_staking"), None).await; + indexer_wait_for_checkpoint(store, 1).await; let (sender, keypair): (_, AccountKeyPair) = get_key_pair(); @@ -102,16 +103,12 @@ fn test_staking() { #[test] fn test_unstaking() { - let ApiTestSetup { - runtime, - store, - client, - cluster, - } = ApiTestSetup::get_or_init(); - - let indexer_client = client; + let ApiTestSetup { runtime, .. } = ApiTestSetup::get_or_init(); runtime.block_on(async move { + let (cluster, store, client) = + &start_test_cluster_with_read_write_indexer(None, Some("test_unstaking"), None).await; + indexer_wait_for_checkpoint(store, 1).await; let (sender, keypair): (_, AccountKeyPair) = get_key_pair(); @@ -137,10 +134,10 @@ fn test_unstaking() { indexer_wait_for_object(client, iota_coin_ref.0, iota_coin_ref.1).await; // Check StakedIota object before test - let staked_iota: Vec = indexer_client.get_stakes(sender).await.unwrap(); + let staked_iota: Vec = client.get_stakes(sender).await.unwrap(); assert!(staked_iota.is_empty()); - let validator = indexer_client + let validator = client .get_latest_iota_system_state() .await .unwrap() @@ -148,7 +145,7 @@ fn test_unstaking() { .iota_address; // Delegate some IOTA - let transaction_bytes: TransactionBlockBytes = indexer_client + let transaction_bytes: TransactionBlockBytes = client .request_add_stake( sender, vec![iota_coin_ref.0], @@ -169,7 +166,7 @@ fn test_unstaking() { indexer_wait_for_latest_checkpoint(store, cluster).await; // Check DelegatedStake object - let staked_iota: Vec = indexer_client.get_stakes(sender).await.unwrap(); + let staked_iota: Vec = client.get_stakes(sender).await.unwrap(); assert_eq!(1, staked_iota.len()); assert_eq!(1000000000, staked_iota[0].stakes[0].principal); assert!(matches!( @@ -179,7 +176,7 @@ fn test_unstaking() { } )); - let transaction_bytes: TransactionBlockBytes = indexer_client + let transaction_bytes: TransactionBlockBytes = client .request_withdraw_stake( sender, staked_iota[0].stakes[0].staked_iota_id, @@ -197,38 +194,29 @@ fn test_unstaking() { cluster.force_new_epoch().await; indexer_wait_for_latest_checkpoint(store, cluster).await; - let node_response = cluster - .rpc_client() - .get_stakes_by_ids(vec![staked_iota[0].stakes[0].staked_iota_id]) - .await - .unwrap(); - assert_eq!(1, node_response.len()); - assert!(matches!( - node_response[0].stakes[0].status, - StakeStatus::Unstaked - )); - - let indexer_response = indexer_client + let indexer_response = client .get_stakes_by_ids(vec![staked_iota[0].stakes[0].staked_iota_id]) .await .unwrap(); assert_eq!(0, indexer_response.len()); - let staked_iota: Vec = indexer_client.get_stakes(sender).await.unwrap(); + let staked_iota: Vec = client.get_stakes(sender).await.unwrap(); assert!(staked_iota.is_empty()); }); } #[test] fn test_timelocked_staking() { - let ApiTestSetup { - runtime, - store, - client, - cluster, - } = ApiTestSetup::get_or_init(); + let ApiTestSetup { runtime, .. } = ApiTestSetup::get_or_init(); runtime.block_on(async move { + let (cluster, store, client) = &start_test_cluster_with_read_write_indexer( + None, + Some("test_timelocked_staking"), + None, + ) + .await; + indexer_wait_for_checkpoint(store, 1).await; let (sender, keypair): (_, AccountKeyPair) = get_key_pair(); @@ -315,22 +303,32 @@ fn test_timelocked_staking() { cluster.force_new_epoch().await; indexer_wait_for_latest_checkpoint(store, cluster).await; - let response = client.get_timelocked_stakes(sender).await.unwrap(); + let staked_iota: Vec = + client.get_timelocked_stakes(sender).await.unwrap(); - assert_eq!(response.len(), 1); + assert_eq!(staked_iota.len(), 1); + assert_eq!(10000000000, staked_iota[0].stakes[0].principal); + assert!(matches!( + staked_iota[0].stakes[0].status, + StakeStatus::Active { + estimated_reward: _ + } + )); }); } #[test] fn test_timelocked_unstaking() { - let ApiTestSetup { - runtime, - store, - client, - cluster, - } = ApiTestSetup::get_or_init(); + let ApiTestSetup { runtime, .. } = ApiTestSetup::get_or_init(); runtime.block_on(async move { + let (cluster, store, client) = &start_test_cluster_with_read_write_indexer( + None, + Some("test_timelocked_unstaking"), + None, + ) + .await; + indexer_wait_for_checkpoint(store, 1).await; let (sender, keypair): (_, AccountKeyPair) = get_key_pair(); @@ -417,11 +415,18 @@ fn test_timelocked_unstaking() { cluster.force_new_epoch().await; indexer_wait_for_latest_checkpoint(store, cluster).await; - let response = client.get_timelocked_stakes(sender).await.unwrap(); + let staked_iota = client.get_timelocked_stakes(sender).await.unwrap(); - assert_eq!(response.len(), 1); + assert_eq!(staked_iota.len(), 1); + assert_eq!(10000000000, staked_iota[0].stakes[0].principal); + assert!(matches!( + staked_iota[0].stakes[0].status, + StakeStatus::Active { + estimated_reward: _ + } + )); - let timelocked_stake_id = response[0].stakes[0].timelocked_staked_iota_id; + let timelocked_stake_id = staked_iota[0].stakes[0].timelocked_staked_iota_id; let timelocked_stake_id_ref = cluster .wallet .get_object_ref(timelocked_stake_id) @@ -463,16 +468,6 @@ fn test_timelocked_unstaking() { let res = client.get_timelocked_stakes(sender).await.unwrap(); assert_eq!(res.len(), 0); - let res = cluster - .rpc_client() - .get_timelocked_stakes_by_ids(vec![timelocked_stake_id]) - .await - .unwrap(); - - assert_eq!(res.len(), 1); - - assert!(matches!(res[0].stakes[0].status, StakeStatus::Unstaked)); - let res = client .get_timelocked_stakes_by_ids(vec![timelocked_stake_id]) .await From 4af119d5d0fece2cde4943886a99b1741b14449f Mon Sep 17 00:00:00 2001 From: Samuel Rufinatscha Date: Thu, 31 Oct 2024 10:31:44 +0100 Subject: [PATCH 14/24] ci: Run RPC tests (#3781) --- .github/workflows/_rust_tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/_rust_tests.yml b/.github/workflows/_rust_tests.yml index 6abe2108be8..ac1b666e20b 100644 --- a/.github/workflows/_rust_tests.yml +++ b/.github/workflows/_rust_tests.yml @@ -243,3 +243,4 @@ jobs: cargo nextest run --no-fail-fast --test-threads 8 --package iota-graphql-e2e-tests --features pg_integration cargo nextest run --no-fail-fast --test-threads 1 --package iota-cluster-test --test local_cluster_test --features pg_integration cargo nextest run --no-fail-fast --test-threads 1 --package iota-indexer --test ingestion_tests --features pg_integration + cargo test --profile simulator --package iota-indexer --test rpc-tests --features shared_test_runtime From 57f72c6381523532a4ea7c35de8c9f75117f61a2 Mon Sep 17 00:00:00 2001 From: Tomasz Pastusiak Date: Thu, 31 Oct 2024 10:34:44 +0100 Subject: [PATCH 15/24] iota-indexer: Add tests for TransactionBuilder api (#3753) --- crates/iota-indexer/tests/rpc-tests/main.rs | 3 + .../tests/rpc-tests/transaction_builder.rs | 477 ++++++++++++++++++ 2 files changed, 480 insertions(+) create mode 100644 crates/iota-indexer/tests/rpc-tests/transaction_builder.rs diff --git a/crates/iota-indexer/tests/rpc-tests/main.rs b/crates/iota-indexer/tests/rpc-tests/main.rs index 3c95735c5ad..0479b12703e 100644 --- a/crates/iota-indexer/tests/rpc-tests/main.rs +++ b/crates/iota-indexer/tests/rpc-tests/main.rs @@ -19,5 +19,8 @@ mod move_utils; #[cfg(feature = "shared_test_runtime")] mod read_api; +#[cfg(feature = "shared_test_runtime")] +mod transaction_builder; + #[cfg(feature = "shared_test_runtime")] mod write_api; diff --git a/crates/iota-indexer/tests/rpc-tests/transaction_builder.rs b/crates/iota-indexer/tests/rpc-tests/transaction_builder.rs new file mode 100644 index 00000000000..2df1f73ca86 --- /dev/null +++ b/crates/iota-indexer/tests/rpc-tests/transaction_builder.rs @@ -0,0 +1,477 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr; + +use diesel::PgConnection; +use iota_indexer::store::PgIndexerStore; +use iota_json::{call_args, type_args}; +use iota_json_rpc_api::{CoinReadApiClient, ReadApiClient, TransactionBuilderClient}; +use iota_json_rpc_types::{ + IotaObjectDataOptions, MoveCallParams, RPCTransactionRequestParams, TransactionBlockBytes, + TransferObjectParams, +}; +use iota_types::{ + IOTA_FRAMEWORK_ADDRESS, + base_types::{IotaAddress, ObjectID}, + crypto::{AccountKeyPair, get_key_pair}, + gas_coin::GAS, + object::Owner, + utils::to_sender_signed_transaction, +}; +use jsonrpsee::http_client::HttpClient; +use test_cluster::TestCluster; + +use crate::common::{ApiTestSetup, indexer_wait_for_object, indexer_wait_for_transaction}; +const FUNDED_BALANCE_PER_COIN: u64 = 10_000_000_000; + +#[test] +fn transfer_object() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + let (sender, keypair): (_, AccountKeyPair) = get_key_pair(); + let (receiver, _): (_, AccountKeyPair) = get_key_pair(); + + let sender_coins = create_coins_and_wait_for_indexer(cluster, client, sender, 2).await; + let gas = sender_coins[0]; + let object_to_send = sender_coins[1]; + + let tx_bytes = client + .transfer_object( + sender, + object_to_send, + Some(gas), + 100_000_000.into(), + receiver, + ) + .await + .unwrap(); + execute_tx_and_wait_for_indexer(client, cluster, store, tx_bytes, &keypair).await; + + let transferred_object = client + .get_object(object_to_send, Some(IotaObjectDataOptions::full_content())) + .await + .unwrap(); + + assert_eq!( + transferred_object.owner(), + Some(Owner::AddressOwner(receiver)) + ); + }); +} + +#[test] +fn transfer_iota() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + let (sender, keypair): (_, AccountKeyPair) = get_key_pair(); + let (receiver, _): (_, AccountKeyPair) = get_key_pair(); + + let sender_coins = create_coins_and_wait_for_indexer(cluster, client, sender, 1).await; + let gas = sender_coins[0]; + let transferred_balance = 100_000; + + let tx_bytes = client + .transfer_iota( + sender, + gas, + 100_000_000.into(), + receiver, + Some(transferred_balance.into()), + ) + .await + .unwrap(); + execute_tx_and_wait_for_indexer(client, cluster, store, tx_bytes, &keypair).await; + + let receiver_balances = get_address_balances(client, receiver).await; + + assert_eq!(receiver_balances, [transferred_balance]); + }); +} + +#[test] +fn pay() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + let (sender, keypair): (_, AccountKeyPair) = get_key_pair(); + let (receiver_1, _): (_, AccountKeyPair) = get_key_pair(); + let (receiver_2, _): (_, AccountKeyPair) = get_key_pair(); + + let input_coins: u64 = 3; + let sender_coins = + create_coins_and_wait_for_indexer(cluster, client, sender, input_coins as u32 + 1) + .await; + let total_input_coins_balance = FUNDED_BALANCE_PER_COIN * input_coins; + let transferred_balance_1 = total_input_coins_balance / 2 - 100; + let transferred_balance_2 = total_input_coins_balance / 2 - 700; + + let tx_bytes = client + .pay( + sender, + sender_coins[0..input_coins as usize].into(), + [receiver_1, receiver_2].into(), + [transferred_balance_1.into(), transferred_balance_2.into()].into(), + None, // let node find the gas automatically + 100_000_000.into(), + ) + .await + .unwrap(); + execute_tx_and_wait_for_indexer(client, cluster, store, tx_bytes, &keypair).await; + + let receiver_1_balances = get_address_balances(client, receiver_1).await; + let receiver_2_balances = get_address_balances(client, receiver_2).await; + + assert_eq!(receiver_1_balances, [transferred_balance_1]); + assert_eq!(receiver_2_balances, [transferred_balance_2]); + }); +} + +#[test] +fn pay_iota() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + let (sender, keypair): (_, AccountKeyPair) = get_key_pair(); + let (receiver_1, _): (_, AccountKeyPair) = get_key_pair(); + let (receiver_2, _): (_, AccountKeyPair) = get_key_pair(); + + let input_coins: u64 = 3; + let sender_coins = + create_coins_and_wait_for_indexer(cluster, client, sender, input_coins as u32).await; + let gas_budget = 100_000_000; + let total_available_input_coins_balance: u64 = + FUNDED_BALANCE_PER_COIN * input_coins - gas_budget; + let transferred_balance_1 = total_available_input_coins_balance / 2 - 100; + let transferred_balance_2 = total_available_input_coins_balance / 2 - 700; + + let tx_bytes = client + .pay_iota( + sender, + sender_coins, + [receiver_1, receiver_2].into(), + [transferred_balance_1.into(), transferred_balance_2.into()].into(), + gas_budget.into(), + ) + .await + .unwrap(); + execute_tx_and_wait_for_indexer(client, cluster, store, tx_bytes, &keypair).await; + + let receiver_1_balances = get_address_balances(client, receiver_1).await; + let receiver_2_balances = get_address_balances(client, receiver_2).await; + + assert_eq!(receiver_1_balances, [transferred_balance_1]); + assert_eq!(receiver_2_balances, [transferred_balance_2]); + }); +} + +#[test] +fn pay_all_iota() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + let (sender, keypair): (_, AccountKeyPair) = get_key_pair(); + let (receiver, _): (_, AccountKeyPair) = get_key_pair(); + + let input_coins: u64 = 3; + let sender_coins = + create_coins_and_wait_for_indexer(cluster, client, sender, input_coins as u32).await; + let gas_budget = 100_000_000; + + let tx_bytes = client + .pay_all_iota(sender, sender_coins, receiver, gas_budget.into()) + .await + .unwrap(); + execute_tx_and_wait_for_indexer(client, cluster, store, tx_bytes, &keypair).await; + + let receiver_balances = get_address_balances(client, receiver).await; + let expected_minimum_receiver_balance = FUNDED_BALANCE_PER_COIN * input_coins - gas_budget; + + assert_eq!(receiver_balances.len(), 1); + assert!(receiver_balances[0] >= expected_minimum_receiver_balance); + }); +} + +#[test] +fn move_call() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + + runtime + .block_on(async move { + let (sender, keypair): (_, AccountKeyPair) = get_key_pair(); + + let sender_coins = create_coins_and_wait_for_indexer(cluster, client, sender, 3).await; + let gas = sender_coins[0]; + + let tx_bytes = client + .move_call( + sender, + ObjectID::new(IOTA_FRAMEWORK_ADDRESS.into_bytes()), + "coin".to_string(), + "join".to_string(), + type_args![GAS::type_tag()].unwrap(), + call_args!(sender_coins[1], sender_coins[2]).unwrap(), + Some(gas), + 10_000_000.into(), + None, + ) + .await + .unwrap(); + execute_tx_and_wait_for_indexer(client, cluster, store, tx_bytes, &keypair).await; + + let mut sender_balances = get_address_balances(client, sender).await; + sender_balances.sort(); + + assert_eq!(sender_balances[1], FUNDED_BALANCE_PER_COIN * 2); + + Ok::<(), anyhow::Error>(()) + }) + .unwrap(); +} + +#[test] +fn split_coin() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + let (sender, keypair): (_, AccountKeyPair) = get_key_pair(); + + let sender_coins = create_coins_and_wait_for_indexer(cluster, client, sender, 2).await; + let split_amount_1 = 100_000; + let split_amount_2 = 20_000; + let split_amount_3 = 30_000; + let gas_budget = 100_000_000; + + let tx_bytes = client + .split_coin( + sender, + sender_coins[0], + vec![ + split_amount_1.into(), + split_amount_2.into(), + split_amount_3.into(), + ], + None, + gas_budget.into(), + ) + .await + .unwrap(); + execute_tx_and_wait_for_indexer(client, cluster, store, tx_bytes, &keypair).await; + + let mut sender_balances = get_address_balances(client, sender).await; + sender_balances.sort(); + + assert_eq!(sender_balances[0..3], [ + split_amount_2, + split_amount_3, + split_amount_1, + ]); + }); +} + +#[test] +fn split_coin_equal() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + let (sender, keypair): (_, AccountKeyPair) = get_key_pair(); + + let sender_coins = create_coins_and_wait_for_indexer(cluster, client, sender, 2).await; + let gas_budget = 100_000_000; + + let tx_bytes = client + .split_coin_equal(sender, sender_coins[0], 3.into(), None, gas_budget.into()) + .await + .unwrap(); + execute_tx_and_wait_for_indexer(client, cluster, store, tx_bytes, &keypair).await; + + let mut sender_balances = get_address_balances(client, sender).await; + sender_balances.sort(); + + assert_eq!(sender_balances[0..3], [ + 3_333_333_333, + 3_333_333_333, + 3_333_333_334, + ]); + }); +} + +#[test] +fn merge_coin() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + + runtime.block_on(async move { + let (sender, keypair): (_, AccountKeyPair) = get_key_pair(); + + let sender_coins = create_coins_and_wait_for_indexer(cluster, client, sender, 3).await; + let gas_budget = 100_000_000; + + let tx_bytes = client + .merge_coin( + sender, + sender_coins[0], + sender_coins[1], + Some(sender_coins[2]), + gas_budget.into(), + ) + .await + .unwrap(); + execute_tx_and_wait_for_indexer(client, cluster, store, tx_bytes, &keypair).await; + + let mut sender_balances = get_address_balances(client, sender).await; + sender_balances.sort(); + + assert_eq!(sender_balances.len(), 2); + assert_eq!(sender_balances[1], FUNDED_BALANCE_PER_COIN * 2); + }); +} + +#[test] +fn batch_transaction() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + + runtime + .block_on(async move { + let (sender, keypair): (_, AccountKeyPair) = get_key_pair(); + let (receiver, _): (_, AccountKeyPair) = get_key_pair(); + + let sender_coins = create_coins_and_wait_for_indexer(cluster, client, sender, 3).await; + let gas = sender_coins[0]; + let coin_to_split = sender_coins[1]; + let coin_to_transfer: ObjectID = sender_coins[2]; + let amount_to_split = FUNDED_BALANCE_PER_COIN / 2 - 123_000; + let amount_to_leave = FUNDED_BALANCE_PER_COIN - amount_to_split; + + let tx_bytes: TransactionBlockBytes = client + .batch_transaction( + sender, + vec![ + RPCTransactionRequestParams::MoveCallRequestParams(MoveCallParams { + package_object_id: ObjectID::new(IOTA_FRAMEWORK_ADDRESS.into_bytes()), + module: "pay".to_string(), + function: "split".to_string(), + type_arguments: type_args![GAS::type_tag()]?, + arguments: call_args!(coin_to_split, amount_to_split)?, + }), + RPCTransactionRequestParams::TransferObjectRequestParams( + TransferObjectParams { + recipient: receiver, + object_id: coin_to_transfer, + }, + ), + ], + Some(gas), + 10_000_000.into(), + None, + ) + .await?; + execute_tx_and_wait_for_indexer(client, cluster, store, tx_bytes, &keypair).await; + + let mut sender_balances = get_address_balances(client, sender).await; + let receiver_balances = get_address_balances(client, receiver).await; + sender_balances.sort(); + + assert_eq!(sender_balances.len(), 3); + assert_eq!(sender_balances[0..2], [amount_to_split, amount_to_leave]); + assert_eq!(receiver_balances, [FUNDED_BALANCE_PER_COIN]); + + Ok::<(), anyhow::Error>(()) + }) + .unwrap(); +} + +async fn execute_tx_and_wait_for_indexer( + indexer_client: &HttpClient, + cluster: &TestCluster, + store: &PgIndexerStore, + tx_bytes: TransactionBlockBytes, + keypair: &AccountKeyPair, +) { + let txn = to_sender_signed_transaction(tx_bytes.to_data().unwrap(), keypair); + let res = cluster.wallet.execute_transaction_must_succeed(txn).await; + indexer_wait_for_transaction(res.digest, store, indexer_client).await; +} + +async fn get_address_balances(indexer_client: &HttpClient, address: IotaAddress) -> Vec { + indexer_client + .get_coins(address, None, None, None) + .await + .unwrap() + .data + .iter() + .map(|coin| coin.balance) + .collect() +} + +async fn create_coins_and_wait_for_indexer( + cluster: &TestCluster, + indexer_client: &HttpClient, + address: IotaAddress, + objects_count: u32, +) -> Vec { + let mut coins: Vec = Vec::new(); + for _ in 0..objects_count { + let coin = cluster + .fund_address_and_return_gas( + cluster.get_reference_gas_price().await, + Some(FUNDED_BALANCE_PER_COIN), + address, + ) + .await; + indexer_wait_for_object(indexer_client, coin.0, coin.1).await; + coins.push(coin.0); + } + coins +} From a61ad41776567cd058e7dfca56c66c9f1f68d0be Mon Sep 17 00:00:00 2001 From: Sergiu Popescu Date: Thu, 31 Oct 2024 21:43:16 +0100 Subject: [PATCH 16/24] fix(iota-indexer): update README.md --- crates/iota-indexer/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/iota-indexer/README.md b/crates/iota-indexer/README.md index ffdcd0672a9..6f4ddfa7e0f 100644 --- a/crates/iota-indexer/README.md +++ b/crates/iota-indexer/README.md @@ -111,6 +111,10 @@ For a better testing experience is possible to use [nextest](https://nexte.st/) > [!NOTE] > rpc tests which rely on a shared runtime are not supported with `nextest` +> +> This is because `cargo nextest` process-per-test execution model makes extremely difficult to share state and resources between tests. +> +> On the other hand `cargo test` does not run tests in separate processes by default. This means that tests can share state and resources. ```sh # run tests requiring only postgres integration From d3b3fc5fa4a1c04d1b63132096f3a6800dfb0006 Mon Sep 17 00:00:00 2001 From: Sergiu Popescu Date: Thu, 31 Oct 2024 21:53:06 +0100 Subject: [PATCH 17/24] fix(iota-indexer): cargo fmt --- crates/iota-indexer/src/models/transactions.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/iota-indexer/src/models/transactions.rs b/crates/iota-indexer/src/models/transactions.rs index 793b13f28bf..3ac8ec6d932 100644 --- a/crates/iota-indexer/src/models/transactions.rs +++ b/crates/iota-indexer/src/models/transactions.rs @@ -442,13 +442,13 @@ impl StoredTransaction { } else { None }; - - let raw_effects = options + + let raw_effects = options .show_raw_effects .then_some(self.raw_effects) .unwrap_or_default(); - Ok(IotaTransactionBlockResponse { + Ok(IotaTransactionBlockResponse { digest: tx_digest, transaction, raw_transaction, From f785fa0354663faee2b62f1b7d739d1936f154c1 Mon Sep 17 00:00:00 2001 From: Sergiu Popescu Date: Fri, 1 Nov 2024 10:30:41 +0100 Subject: [PATCH 18/24] fix(iota-indexer): remove unused deps --- Cargo.lock | 1 - crates/iota-indexer/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3942a8df648..d3f697affcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6823,7 +6823,6 @@ dependencies = [ "serde", "serde_json", "serde_with", - "serial_test", "simulacrum", "tap", "telemetry-subscribers", diff --git a/crates/iota-indexer/Cargo.toml b/crates/iota-indexer/Cargo.toml index 6db4846a164..0ae7e5570c3 100644 --- a/crates/iota-indexer/Cargo.toml +++ b/crates/iota-indexer/Cargo.toml @@ -69,7 +69,6 @@ bundled-mysql = ["mysqlclient-sys?/bundled"] # external dependencies rand.workspace = true serde_json.workspace = true -serial_test = "2.0" # internal dependencies iota-config.workspace = true From 1edae7ae71426cb5ffe631398e1394184b8545b0 Mon Sep 17 00:00:00 2001 From: Sergiu Popescu Date: Mon, 4 Nov 2024 09:11:49 +0100 Subject: [PATCH 19/24] fix(ci): add comment --- .github/workflows/_rust_tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/_rust_tests.yml b/.github/workflows/_rust_tests.yml index ac1b666e20b..3b8db8fd126 100644 --- a/.github/workflows/_rust_tests.yml +++ b/.github/workflows/_rust_tests.yml @@ -237,6 +237,8 @@ jobs: - run: docker restart --time 0 postgres_container - run: sleep 5 - name: tests-requiring-postgres + # Iota-indexer's RPC tests, which depend on a shared runtime, are incompatible with nextest due to its process-per-test execution model. + # cargo test, on the other hand, allows tests to share state and resources by default. run: | cargo nextest run --no-fail-fast --test-threads 1 --package iota-graphql-rpc --test e2e_tests --test examples_validation_tests --features pg_integration cargo nextest run --no-fail-fast --test-threads 1 --package iota-graphql-rpc --lib --features pg_integration -- test_query_cost From 115a3009a06c575b2f4d57e6ecf66e2594ada76e Mon Sep 17 00:00:00 2001 From: Sergiu Popescu Date: Mon, 4 Nov 2024 13:23:11 +0100 Subject: [PATCH 20/24] fixup! fix(iota-indexer): update README.md --- crates/iota-indexer/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/iota-indexer/README.md b/crates/iota-indexer/README.md index 6f4ddfa7e0f..929c89f1f41 100644 --- a/crates/iota-indexer/README.md +++ b/crates/iota-indexer/README.md @@ -102,9 +102,9 @@ The crate provides following tests currently: ```sh # run tests requiring only postgres integration -cargo nextest run --features pg_integration --test-threads 1 +cargo test --features pg_integration -- --test-threads 1 # run rpc tests with shared runtime -cargo test --features shared_test_runtime +cargo test --profile simulator --features shared_test_runtime ``` For a better testing experience is possible to use [nextest](https://nexte.st/) From acd34ed592c86b45a8b43e8553e9da2b3f20d43e Mon Sep 17 00:00:00 2001 From: Sergiu Popescu <44298302+sergiupopescu199@users.noreply.github.com> Date: Tue, 5 Nov 2024 13:13:51 +0100 Subject: [PATCH 21/24] Update .github/workflows/_rust_tests.yml Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> --- .github/workflows/_rust_tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/_rust_tests.yml b/.github/workflows/_rust_tests.yml index 3b8db8fd126..59857d2b1a8 100644 --- a/.github/workflows/_rust_tests.yml +++ b/.github/workflows/_rust_tests.yml @@ -237,12 +237,12 @@ jobs: - run: docker restart --time 0 postgres_container - run: sleep 5 - name: tests-requiring-postgres - # Iota-indexer's RPC tests, which depend on a shared runtime, are incompatible with nextest due to its process-per-test execution model. - # cargo test, on the other hand, allows tests to share state and resources by default. run: | cargo nextest run --no-fail-fast --test-threads 1 --package iota-graphql-rpc --test e2e_tests --test examples_validation_tests --features pg_integration cargo nextest run --no-fail-fast --test-threads 1 --package iota-graphql-rpc --lib --features pg_integration -- test_query_cost cargo nextest run --no-fail-fast --test-threads 8 --package iota-graphql-e2e-tests --features pg_integration cargo nextest run --no-fail-fast --test-threads 1 --package iota-cluster-test --test local_cluster_test --features pg_integration cargo nextest run --no-fail-fast --test-threads 1 --package iota-indexer --test ingestion_tests --features pg_integration + # Iota-indexer's RPC tests, which depend on a shared runtime, are incompatible with nextest due to its process-per-test execution model. + # cargo test, on the other hand, allows tests to share state and resources by default. cargo test --profile simulator --package iota-indexer --test rpc-tests --features shared_test_runtime From a4dd8c942e508670b467a38ae63d0c6257f6b0cc Mon Sep 17 00:00:00 2001 From: Sergiu Popescu <44298302+sergiupopescu199@users.noreply.github.com> Date: Tue, 5 Nov 2024 13:14:01 +0100 Subject: [PATCH 22/24] Update crates/iota-indexer/tests/ingestion_tests.rs Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> --- crates/iota-indexer/tests/ingestion_tests.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/iota-indexer/tests/ingestion_tests.rs b/crates/iota-indexer/tests/ingestion_tests.rs index 30f115c10f0..4ed48a2c0f2 100644 --- a/crates/iota-indexer/tests/ingestion_tests.rs +++ b/crates/iota-indexer/tests/ingestion_tests.rs @@ -1,6 +1,7 @@ // Copyright (c) Mysten Labs, Inc. // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 + #[allow(dead_code)] #[cfg(feature = "pg_integration")] mod common; From fc8c44c4980e57aca6d80331c36dc2bde8c63698 Mon Sep 17 00:00:00 2001 From: Samuel Rufinatscha Date: Wed, 6 Nov 2024 15:10:02 +0100 Subject: [PATCH 23/24] fix: Only add a faucet account to the`TestCluster` if there was no `NetworkConfig` provided (#3931) * fix: Don't add a faucet account if the `TestCluster` was built using a `NetworkConfig` * fix: Improve comment * fix: Improve comment * fix: Fmt * fix: Clippy * fix: remove if else --- crates/test-cluster/src/lib.rs | 36 +++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/crates/test-cluster/src/lib.rs b/crates/test-cluster/src/lib.rs index 682a15a5f81..3dec57c6f1b 100644 --- a/crates/test-cluster/src/lib.rs +++ b/crates/test-cluster/src/lib.rs @@ -137,7 +137,7 @@ pub struct TestCluster { pub fullnode_handle: FullNodeHandle, pub bridge_authority_keys: Option>, pub bridge_server_ports: Option>, - faucet: Faucet, + faucet: Option, } impl TestCluster { @@ -813,7 +813,10 @@ impl TestCluster { amount: Option, funding_address: IotaAddress, ) -> ObjectRef { - let Faucet { address, keypair } = &self.faucet; + let Faucet { address, keypair } = &self + .faucet + .as_ref() + .expect("Faucet not initialized: incompatible with `NetworkConfig`."); let keypair = &*keypair.lock().await; @@ -1302,12 +1305,22 @@ impl TestClusterBuilder { } pub async fn build(mut self) -> TestCluster { - // Add a faucet address - let (faucet_address, faucet_keypair): (IotaAddress, AccountKeyPair) = get_key_pair(); - let accounts = &mut self.get_or_init_genesis_config().accounts; - accounts.push(AccountConfig { - address: Some(faucet_address), - gas_amounts: vec![DEFAULT_GAS_AMOUNT], + // We can add a faucet account to the `GenesisConfig` if there was no + // `NetworkConfig` provided. Only either a `GenesisConfig` or a + // `NetworkConfig` can be used to configure and build the cluster. + let faucet = self.network_config.is_none().then(|| { + let (faucet_address, faucet_keypair): (IotaAddress, AccountKeyPair) = get_key_pair(); + let accounts = &mut self.get_or_init_genesis_config().accounts; + accounts.push(AccountConfig { + address: Some(faucet_address), + gas_amounts: vec![DEFAULT_GAS_AMOUNT], + }); + Faucet { + address: faucet_address, + keypair: Arc::new(tokio::sync::Mutex::new(IotaKeyPair::Ed25519( + faucet_keypair, + ))), + } }); // All test clusters receive a continuous stream of random JWKs. @@ -1372,12 +1385,7 @@ impl TestClusterBuilder { fullnode_handle, bridge_authority_keys: None, bridge_server_ports: None, - faucet: Faucet { - address: faucet_address, - keypair: Arc::new(tokio::sync::Mutex::new(IotaKeyPair::Ed25519( - faucet_keypair, - ))), - }, + faucet, } } From 7ba30440b8648614246c11d3541965c5a6990ca3 Mon Sep 17 00:00:00 2001 From: Sergiu Popescu <44298302+sergiupopescu199@users.noreply.github.com> Date: Thu, 7 Nov 2024 09:39:32 +0100 Subject: [PATCH 24/24] Update crates/iota-indexer/tests/rpc-tests/write_api.rs Co-authored-by: Thibault Martinez --- crates/iota-indexer/tests/rpc-tests/write_api.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/iota-indexer/tests/rpc-tests/write_api.rs b/crates/iota-indexer/tests/rpc-tests/write_api.rs index 9b74f080f35..200e80f66d9 100644 --- a/crates/iota-indexer/tests/rpc-tests/write_api.rs +++ b/crates/iota-indexer/tests/rpc-tests/write_api.rs @@ -23,6 +23,7 @@ use crate::common::{ApiTestSetup, indexer_wait_for_checkpoint, indexer_wait_for_ type TxBytes = Base64; type Signatures = Vec; + async fn prepare_and_sign_tx( sender: IotaAddress, receiver: IotaAddress,