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 + ) + }); +}