diff --git a/crates/iota-indexer/tests/common/mod.rs b/crates/iota-indexer/tests/common/mod.rs index 8f97d1654aa..8d87b97da0a 100644 --- a/crates/iota-indexer/tests/common/mod.rs +++ b/crates/iota-indexer/tests/common/mod.rs @@ -9,10 +9,7 @@ use std::{ }; use diesel::PgConnection; -use iota_config::{ - local_ip_utils::{get_available_port, new_local_tcp_socket_for_testing}, - node::RunWithRange, -}; +use iota_config::local_ip_utils::{get_available_port, new_local_tcp_socket_for_testing}; use iota_indexer::{ IndexerConfig, errors::IndexerError, @@ -26,7 +23,6 @@ use iota_metrics::init_metrics; use iota_types::{ base_types::{ObjectID, SequenceNumber}, digests::TransactionDigest, - object::Object, }; use jsonrpsee::{ http_client::{HttpClient, HttpClientBuilder}, @@ -58,12 +54,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, - Some("shared_test_indexer_db"), - None, - )); + let (cluster, store, client) = runtime.block_on( + start_test_cluster_with_read_write_indexer(Some("shared_test_indexer_db"), None), + ); Self { runtime, @@ -117,24 +110,16 @@ impl SimulacrumTestSetup { /// 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>, - objects: Option>, + builder_modifier: Option TestClusterBuilder>>, ) -> (TestCluster, PgIndexerStore, HttpClient) { let temp = tempdir().unwrap().into_path(); let mut builder = TestClusterBuilder::new().with_data_ingestion_dir(temp.clone()); - // 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, - ))); + if let Some(builder_modifier) = builder_modifier { + builder = builder_modifier(builder); }; - 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 d2ff07927e3..3f662fef1d3 100644 --- a/crates/iota-indexer/tests/rpc-tests/governance_api.rs +++ b/crates/iota-indexer/tests/rpc-tests/governance_api.rs @@ -30,7 +30,7 @@ fn test_staking() { runtime.block_on(async move { let (cluster, store, client) = - &start_test_cluster_with_read_write_indexer(None, Some("test_staking"), None).await; + &start_test_cluster_with_read_write_indexer(Some("test_staking"), None).await; indexer_wait_for_checkpoint(store, 1).await; @@ -107,7 +107,7 @@ fn test_unstaking() { runtime.block_on(async move { let (cluster, store, client) = - &start_test_cluster_with_read_write_indexer(None, Some("test_unstaking"), None).await; + &start_test_cluster_with_read_write_indexer(Some("test_unstaking"), None).await; indexer_wait_for_checkpoint(store, 1).await; @@ -210,12 +210,9 @@ fn test_timelocked_staking() { 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; + let (cluster, store, client) = + &start_test_cluster_with_read_write_indexer(Some("test_timelocked_staking"), None) + .await; indexer_wait_for_checkpoint(store, 1).await; @@ -322,12 +319,9 @@ fn test_timelocked_unstaking() { 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; + let (cluster, store, client) = + &start_test_cluster_with_read_write_indexer(Some("test_timelocked_unstaking"), None) + .await; indexer_wait_for_checkpoint(store, 1).await; diff --git a/crates/iota-indexer/tests/rpc-tests/transaction_builder.rs b/crates/iota-indexer/tests/rpc-tests/transaction_builder.rs index 2df1f73ca86..c541e2ea65f 100644 --- a/crates/iota-indexer/tests/rpc-tests/transaction_builder.rs +++ b/crates/iota-indexer/tests/rpc-tests/transaction_builder.rs @@ -6,23 +6,38 @@ 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_api::{ + CoinReadApiClient, GovernanceReadApiClient, IndexerApiClient, ReadApiClient, + TransactionBuilderClient, +}; use iota_json_rpc_types::{ - IotaObjectDataOptions, MoveCallParams, RPCTransactionRequestParams, TransactionBlockBytes, - TransferObjectParams, + IotaObjectDataOptions, IotaObjectResponseQuery, MoveCallParams, ObjectsPage, + RPCTransactionRequestParams, StakeStatus, TransactionBlockBytes, TransferObjectParams, }; +use iota_protocol_config::ProtocolConfig; +use iota_swarm_config::genesis_config::AccountConfig; use iota_types::{ IOTA_FRAMEWORK_ADDRESS, - base_types::{IotaAddress, ObjectID}, + base_types::{IotaAddress, MoveObjectType, ObjectID}, crypto::{AccountKeyPair, get_key_pair}, + digests::TransactionDigest, gas_coin::GAS, - object::Owner, + id::UID, + object::{Data, MoveObject, OBJECT_START_VERSION, ObjectInner, Owner}, + timelock::{ + label::label_struct_tag_to_string, stardust_upgrade_label::stardust_upgrade_label_type, + timelock::TimeLock, + }, 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}; +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, +}; const FUNDED_BALANCE_PER_COIN: u64 = 10_000_000_000; #[test] @@ -432,6 +447,331 @@ fn batch_transaction() { .unwrap(); } +#[test] +fn request_add_stake() { + let ApiTestSetup { runtime, .. } = ApiTestSetup::get_or_init(); + + runtime + .block_on(async move { + let (cluster, store, client) = &start_test_cluster_with_read_write_indexer( + Some("transaction_builder_request_add_stake"), + None, + ) + .await; + let (address, keypair): (_, AccountKeyPair) = get_key_pair(); + let coins = create_coins_and_wait_for_indexer(cluster, client, address, 4).await; + let gas = coins[3]; + let coins_to_stake = coins[..3].to_vec(); + let validator = get_validator(client).await; + // subtracting some amount to see if it is possible to stake smaller amount than + // is provided in the input coins + let stake_amount = FUNDED_BALANCE_PER_COIN * 3 - 10_000; + + let tx_bytes: TransactionBlockBytes = client + .request_add_stake( + address, + coins_to_stake, + Some(stake_amount.into()), + validator, + Some(gas), + 100_000_000.into(), + ) + .await + .unwrap(); + execute_tx_and_wait_for_indexer(client, cluster, store, tx_bytes, &keypair).await; + + let staked_iota = client.get_stakes(address).await.unwrap(); + + assert_eq!(1, staked_iota.len()); + let staked_iota = &staked_iota[0]; + assert_eq!(validator, staked_iota.validator_address); + + assert_eq!(1, staked_iota.stakes.len()); + let stake = &staked_iota.stakes[0]; + assert!(matches!(stake.status, StakeStatus::Pending)); + assert_eq!(stake.principal, stake_amount); + + cluster.force_new_epoch().await; + indexer_wait_for_latest_checkpoint(store, cluster).await; + let staked_iota = client.get_stakes(address).await.unwrap(); + let stake = &staked_iota[0].stakes[0]; + assert!(matches!(stake.status, StakeStatus::Active { .. })); + + Ok::<(), anyhow::Error>(()) + }) + .unwrap(); +} + +#[test] +fn request_withdraw_stake_from_pending() { + let ApiTestSetup { + runtime, + store, + client, + cluster, + } = ApiTestSetup::get_or_init(); + + runtime + .block_on(async move { + let (address, keypair): (_, AccountKeyPair) = get_key_pair(); + let coins = create_coins_and_wait_for_indexer(cluster, client, address, 4).await; + let gas = coins[3]; + let coins_to_stake = coins[..3].to_vec(); + let validator = get_validator(client).await; + // subtracting some amount to see if it is possible to stake smaller amount than + // is provided in the input coins + let stake_amount = FUNDED_BALANCE_PER_COIN * 3 - 10_000; + + let tx_bytes: TransactionBlockBytes = client + .request_add_stake( + address, + coins_to_stake, + Some(stake_amount.into()), + validator, + Some(gas), + 100_000_000.into(), + ) + .await + .unwrap(); + execute_tx_and_wait_for_indexer(client, cluster, store, tx_bytes, &keypair).await; + + let staked_iota = client.get_stakes(address).await.unwrap(); + let stake = &staked_iota[0].stakes[0]; + assert!(matches!(stake.status, StakeStatus::Pending)); + + let tx_bytes: TransactionBlockBytes = client + .request_withdraw_stake( + address, + stake.staked_iota_id, + Some(gas), + 100_000_000.into(), + ) + .await + .unwrap(); + execute_tx_and_wait_for_indexer(client, cluster, store, tx_bytes, &keypair).await; + + let staked_iota = client.get_stakes(address).await.unwrap(); + assert!(staked_iota.is_empty()); + + Ok::<(), anyhow::Error>(()) + }) + .unwrap(); +} + +#[test] +fn request_withdraw_stake_from_active() { + let ApiTestSetup { runtime, .. } = ApiTestSetup::get_or_init(); + + runtime + .block_on(async move { + let (cluster, store, client) = &start_test_cluster_with_read_write_indexer( + Some("transaction_builder_request_withdraw_stake_from_active"), + None, + ) + .await; + let (address, keypair): (_, AccountKeyPair) = get_key_pair(); + let coins = create_coins_and_wait_for_indexer(cluster, client, address, 4).await; + let gas = coins[3]; + let coins_to_stake = coins[..3].to_vec(); + let validator = get_validator(client).await; + // subtracting some amount to see if it is possible to stake smaller amount than + // is provided in the input coins + let stake_amount = FUNDED_BALANCE_PER_COIN * 3 - 10_000; + + let tx_bytes: TransactionBlockBytes = client + .request_add_stake( + address, + coins_to_stake, + Some(stake_amount.into()), + validator, + Some(gas), + 100_000_000.into(), + ) + .await + .unwrap(); + execute_tx_and_wait_for_indexer(client, cluster, store, tx_bytes, &keypair).await; + + cluster.force_new_epoch().await; + indexer_wait_for_latest_checkpoint(store, cluster).await; + let staked_iota = client.get_stakes(address).await.unwrap(); + let stake = &staked_iota[0].stakes[0]; + assert!(matches!(stake.status, StakeStatus::Active { .. })); + + let tx_bytes: TransactionBlockBytes = client + .request_withdraw_stake( + address, + stake.staked_iota_id, + Some(gas), + 100_000_000.into(), + ) + .await + .unwrap(); + execute_tx_and_wait_for_indexer(client, cluster, store, tx_bytes, &keypair).await; + + let staked_iota = client.get_stakes(address).await.unwrap(); + assert!(staked_iota.is_empty()); + + Ok::<(), anyhow::Error>(()) + }) + .unwrap(); +} + +#[test] +fn request_add_timelocked_stake() { + let ApiTestSetup { runtime, .. } = ApiTestSetup::get_or_init(); + + runtime + .block_on(async move { + let (address, keypair): (_, AccountKeyPair) = get_key_pair(); + let (cluster, store, client, timelocked_balance) = create_cluster_with_timelocked_iota( + address, + "transaction_builder_request_add_timelocked_stake", + ) + .await; + indexer_wait_for_checkpoint(&store, 1).await; + + let coin = get_gas_object_id(&client, address).await; + let validator = get_validator(&client).await; + + let tx_bytes: TransactionBlockBytes = client + .request_add_timelocked_stake( + address, + timelocked_balance, + validator, + coin, + 100_000_000.into(), + ) + .await + .unwrap(); + execute_tx_and_wait_for_indexer(&client, &cluster, &store, tx_bytes, &keypair).await; + + let staked_iota = client.get_timelocked_stakes(address).await.unwrap(); + + assert_eq!(1, staked_iota.len()); + let staked_iota = &staked_iota[0]; + assert_eq!(validator, staked_iota.validator_address); + + assert_eq!(1, staked_iota.stakes.len()); + let stake = &staked_iota.stakes[0]; + assert!(matches!(stake.status, StakeStatus::Pending)); + + cluster.force_new_epoch().await; + indexer_wait_for_latest_checkpoint(&store, &cluster).await; + let staked_iota = client.get_timelocked_stakes(address).await.unwrap(); + let stake = &staked_iota[0].stakes[0]; + assert!(matches!(stake.status, StakeStatus::Active { .. })); + + Ok::<(), anyhow::Error>(()) + }) + .unwrap(); +} + +#[test] +fn request_withdraw_timelocked_stake_from_pending() { + let ApiTestSetup { runtime, .. } = ApiTestSetup::get_or_init(); + + runtime + .block_on(async move { + let (address, keypair): (_, AccountKeyPair) = get_key_pair(); + let (cluster, store, client, timelocked_balance) = create_cluster_with_timelocked_iota( + address, + "transaction_builder_request_withdraw_timelocked_stake_from_pending", + ) + .await; + indexer_wait_for_checkpoint(&store, 1).await; + + let coin = get_gas_object_id(&client, address).await; + let validator = get_validator(&client).await; + + let tx_bytes: TransactionBlockBytes = client + .request_add_timelocked_stake( + address, + timelocked_balance, + validator, + coin, + 100_000_000.into(), + ) + .await + .unwrap(); + execute_tx_and_wait_for_indexer(&client, &cluster, &store, tx_bytes, &keypair).await; + + let staked_iota = client.get_timelocked_stakes(address).await.unwrap(); + let stake = &staked_iota[0].stakes[0]; + assert!(matches!(stake.status, StakeStatus::Pending)); + + let tx_bytes: TransactionBlockBytes = client + .request_withdraw_timelocked_stake( + address, + stake.timelocked_staked_iota_id, + coin, + 100_000_000.into(), + ) + .await + .unwrap(); + execute_tx_and_wait_for_indexer(&client, &cluster, &store, tx_bytes, &keypair).await; + + let staked_iota = client.get_timelocked_stakes(address).await.unwrap(); + assert!(staked_iota.is_empty()); + + Ok::<(), anyhow::Error>(()) + }) + .unwrap(); +} + +#[test] +fn request_withdraw_timelocked_stake_from_active() { + let ApiTestSetup { runtime, .. } = ApiTestSetup::get_or_init(); + + runtime + .block_on(async move { + let (address, keypair): (_, AccountKeyPair) = get_key_pair(); + let (cluster, store, client, timelocked_balance) = create_cluster_with_timelocked_iota( + address, + "transaction_builder_request_withdraw_timelocked_stake_from_active", + ) + .await; + indexer_wait_for_checkpoint(&store, 1).await; + + let coin = get_gas_object_id(&client, address).await; + let validator = get_validator(&client).await; + + let tx_bytes: TransactionBlockBytes = client + .request_add_timelocked_stake( + address, + timelocked_balance, + validator, + coin, + 100_000_000.into(), + ) + .await + .unwrap(); + execute_tx_and_wait_for_indexer(&client, &cluster, &store, tx_bytes, &keypair).await; + + cluster.force_new_epoch().await; + indexer_wait_for_latest_checkpoint(&store, &cluster).await; + let staked_iota = client.get_timelocked_stakes(address).await.unwrap(); + let stake = &staked_iota[0].stakes[0]; + assert!(matches!(stake.status, StakeStatus::Active { .. })); + + let tx_bytes: TransactionBlockBytes = client + .request_withdraw_timelocked_stake( + address, + stake.timelocked_staked_iota_id, + coin, + 100_000_000.into(), + ) + .await + .unwrap(); + execute_tx_and_wait_for_indexer(&client, &cluster, &store, tx_bytes, &keypair).await; + + let staked_iota = client.get_timelocked_stakes(address).await.unwrap(); + assert!(staked_iota.is_empty()); + + Ok::<(), anyhow::Error>(()) + }) + .unwrap(); +} + async fn execute_tx_and_wait_for_indexer( indexer_client: &HttpClient, cluster: &TestCluster, @@ -475,3 +815,100 @@ async fn create_coins_and_wait_for_indexer( } coins } + +async fn create_cluster_with_timelocked_iota( + address: IotaAddress, + indexer_db_name: &str, +) -> ( + TestCluster, + PgIndexerStore, + HttpClient, + ObjectID, +) { + let principal = 100_000_000_000; + let expiration_timestamp_ms = u64::MAX; + let label = Option::Some(label_struct_tag_to_string(stardust_upgrade_label_type())); + + let timelock_iota = unsafe { + MoveObject::new_from_execution( + MoveObjectType::timelocked_iota_balance(), + false, + OBJECT_START_VERSION, + TimeLock::::new( + UID::new(ObjectID::random()), + iota_types::balance::Balance::new(principal), + expiration_timestamp_ms, + label.clone(), + ) + .to_bcs_bytes(), + &ProtocolConfig::get_for_min_version(), + ) + .unwrap() + }; + let timelock_iota = ObjectInner { + owner: Owner::AddressOwner(address), + data: Data::Move(timelock_iota), + previous_transaction: TransactionDigest::genesis_marker(), + storage_rebate: 0, + }; + + let (cluster, store, client) = start_test_cluster_with_read_write_indexer( + Some(indexer_db_name), + Some(Box::new(move |builder| { + builder + .with_accounts( + [AccountConfig { + address: Some(address), + gas_amounts: [1_000_000_000].into(), + }] + .into(), + ) + .with_objects([timelock_iota.into()]) + })), + ) + .await; + + let fullnode_client = cluster.rpc_client(); + + let objects: ObjectsPage = fullnode_client + .get_owned_objects( + address, + Some(IotaObjectResponseQuery::new_with_options( + IotaObjectDataOptions::full_content(), + )), + None, + None, + ) + .await + .unwrap(); + assert_eq!(2, objects.data.len()); + + let timelocked_balance = objects + .data + .into_iter() + .find(|o| !o.data.as_ref().unwrap().is_gas_coin()) + .unwrap() + .object() + .unwrap() + .object_id; + + (cluster, store, client, timelocked_balance) +} + +async fn get_validator(client: &HttpClient) -> IotaAddress { + client + .get_latest_iota_system_state() + .await + .unwrap() + .active_validators[0] + .iota_address +} + +async fn get_gas_object_id(client: &HttpClient, address: IotaAddress) -> ObjectID { + client + .get_coins(address, None, None, None) + .await + .unwrap() + .data[0] + .coin_object_id +}